зеркало из https://github.com/Azure/vagrant-azure.git
Merge pull request #152 from matt-richardson/support-winrm
Support Windows & WinRM [WIP]
This commit is contained in:
Коммит
96da70c61d
|
@ -75,6 +75,16 @@ module VagrantPlugins
|
|||
end
|
||||
end
|
||||
|
||||
# This action is called to read the WinRM info of the machine. The
|
||||
# resulting state is expected to be put into the `:machine_winrm_info` key.
|
||||
def self.action_read_winrm_info
|
||||
Vagrant::Action::Builder.new.tap do |b|
|
||||
b.use ConfigValidate
|
||||
b.use ConnectAzure
|
||||
b.use ReadWinrmInfo
|
||||
end
|
||||
end
|
||||
|
||||
# This action is called to read the state of the machine. The
|
||||
# resulting state is expected to be put into the `:machine_state_id`
|
||||
# key.
|
||||
|
@ -178,6 +188,7 @@ module VagrantPlugins
|
|||
autoload :MessageNotCreated, action_root.join('message_not_created')
|
||||
autoload :MessageWillNotDestroy, action_root.join('message_will_not_destroy')
|
||||
autoload :ReadSSHInfo, action_root.join('read_ssh_info')
|
||||
autoload :ReadWinrmInfo, action_root.join('read_winrm_info')
|
||||
autoload :ReadState, action_root.join('read_state')
|
||||
autoload :RestartVM, action_root.join('restart_vm')
|
||||
autoload :RunInstance, action_root.join('run_instance')
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#--------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Open Technologies, Inc.
|
||||
# All Rights Reserved. Licensed under the Apache License, Version 2.0.
|
||||
# See License.txt in the project root for license information.
|
||||
#--------------------------------------------------------------------------
|
||||
require 'log4r'
|
||||
require 'vagrant-azure/util/machine_id_helper'
|
||||
|
||||
module VagrantPlugins
|
||||
module Azure
|
||||
module Action
|
||||
class ReadWinrmInfo
|
||||
include VagrantPlugins::Azure::Util::MachineIdHelper
|
||||
|
||||
def initialize(app, env)
|
||||
@app = app
|
||||
@logger = Log4r::Logger.new('vagrant_azure::action::read_winrm_info')
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if env[:machine].config.vm.guest == :windows
|
||||
env[:machine_winrm_info] = read_winrm_info(env[:azure_arm_service], env)
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def read_winrm_info(azure, env)
|
||||
return nil if env[:machine].id.nil?
|
||||
parsed = parse_machine_id(env[:machine].id)
|
||||
public_ip = azure.network.public_ipaddresses.get(parsed[:group], "#{parsed[:name]}-vagrantPublicIP").value!.body
|
||||
|
||||
{:host => public_ip.properties.dns_settings.fqdn, :port => env[:machine].config.winrm.port}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -28,19 +28,25 @@ module VagrantPlugins
|
|||
machine = env[:machine]
|
||||
|
||||
# Get the configs
|
||||
config = machine.provider_config
|
||||
endpoint = config.endpoint
|
||||
resource_group_name = config.resource_group_name
|
||||
location = config.location
|
||||
admin_user_name = machine.config.ssh.username
|
||||
vm_name = config.vm_name
|
||||
vm_password = config.vm_password
|
||||
vm_size = config.vm_size
|
||||
vm_image_urn = config.vm_image_urn
|
||||
virtual_network_name = config.virtual_network_name
|
||||
subnet_name = config.subnet_name
|
||||
tcp_endpoints = config.tcp_endpoints
|
||||
availability_set_name = config.availability_set_name
|
||||
config = machine.provider_config
|
||||
endpoint = config.endpoint
|
||||
resource_group_name = config.resource_group_name
|
||||
location = config.location
|
||||
ssh_user_name = machine.config.ssh.username
|
||||
vm_name = config.vm_name
|
||||
vm_password = config.vm_password
|
||||
vm_size = config.vm_size
|
||||
vm_image_urn = config.vm_image_urn
|
||||
virtual_network_name = config.virtual_network_name
|
||||
subnet_name = config.subnet_name
|
||||
tcp_endpoints = config.tcp_endpoints
|
||||
availability_set_name = config.availability_set_name
|
||||
admin_user_name = config.admin_username
|
||||
admin_password = config.admin_password
|
||||
winrm_port = machine.config.winrm.port
|
||||
winrm_install_self_signed_cert = config.winrm_install_self_signed_cert
|
||||
dns_label_prefix = Haikunator.haikunate(100)
|
||||
deployment_template = config.deployment_template
|
||||
|
||||
# Launch!
|
||||
env[:ui].info(I18n.t('vagrant_azure.launching_instance'))
|
||||
|
@ -48,7 +54,8 @@ module VagrantPlugins
|
|||
env[:ui].info(" -- Subscription Id: #{config.subscription_id}")
|
||||
env[:ui].info(" -- Resource Group Name: #{resource_group_name}")
|
||||
env[:ui].info(" -- Location: #{location}")
|
||||
env[:ui].info(" -- Admin User Name: #{admin_user_name}") if admin_user_name
|
||||
env[:ui].info(" -- SSH User Name: #{ssh_user_name}") if ssh_user_name
|
||||
env[:ui].info(" -- Admin Username: #{admin_user_name}") if admin_user_name
|
||||
env[:ui].info(" -- VM Name: #{vm_name}")
|
||||
env[:ui].info(" -- VM Size: #{vm_size}")
|
||||
env[:ui].info(" -- Image URN: #{vm_image_urn}")
|
||||
|
@ -56,6 +63,7 @@ module VagrantPlugins
|
|||
env[:ui].info(" -- Subnet Name: #{subnet_name}") if subnet_name
|
||||
env[:ui].info(" -- TCP Endpoints: #{tcp_endpoints}") if tcp_endpoints
|
||||
env[:ui].info(" -- Availability Set Name: #{availability_set_name}") if availability_set_name
|
||||
env[:ui].info(" -- DNS Label Prefix: #{dns_label_prefix}")
|
||||
|
||||
image_publisher, image_offer, image_sku, image_version = vm_image_urn.split(':')
|
||||
|
||||
|
@ -67,8 +75,7 @@ module VagrantPlugins
|
|||
@logger.info("Time to fetch os image details: #{env[:metrics]['get_image_details']}")
|
||||
|
||||
deployment_params = {
|
||||
adminUserName: admin_user_name,
|
||||
dnsLabelPrefix: Haikunator.haikunate(100),
|
||||
dnsLabelPrefix: dns_label_prefix,
|
||||
vmSize: vm_size,
|
||||
vmName: vm_name,
|
||||
imagePublisher: image_publisher,
|
||||
|
@ -76,10 +83,22 @@ module VagrantPlugins
|
|||
imageSku: image_sku,
|
||||
imageVersion: image_version,
|
||||
subnetName: subnet_name,
|
||||
virtualNetworkName: virtual_network_name
|
||||
virtualNetworkName: virtual_network_name,
|
||||
}
|
||||
|
||||
if get_image_os(image_details) != 'Windows'
|
||||
# we need to pass different parameters depending upon the OS
|
||||
operating_system = get_image_os(image_details)
|
||||
|
||||
template_params = {
|
||||
operating_system: operating_system,
|
||||
winrm_install_self_signed_cert: winrm_install_self_signed_cert,
|
||||
winrm_port: winrm_port,
|
||||
dns_label_prefix: dns_label_prefix,
|
||||
location: location,
|
||||
deployment_template: deployment_template
|
||||
}
|
||||
|
||||
if operating_system != 'Windows'
|
||||
private_key_paths = machine.config.ssh.private_key_path
|
||||
if private_key_paths.nil? || private_key_paths.empty?
|
||||
raise I18n.t('vagrant_azure.private_key_not_specified')
|
||||
|
@ -87,13 +106,23 @@ module VagrantPlugins
|
|||
|
||||
paths_to_pub = private_key_paths.map{ |k| File.expand_path( k + '.pub') }.select{ |p| File.exists?(p) }
|
||||
raise I18n.t('vagrant_azure.public_key_path_private_key', private_key_paths.join(', ')) if paths_to_pub.empty?
|
||||
deployment_params.merge!(adminUsername: ssh_user_name)
|
||||
deployment_params.merge!(sshKeyData: File.read(paths_to_pub.first))
|
||||
communicator_message = 'vagrant_azure.waiting_for_ssh'
|
||||
else
|
||||
env[:machine].config.vm.communicator = :winrm
|
||||
machine.config.winrm.port = winrm_port
|
||||
machine.config.winrm.username = admin_user_name
|
||||
machine.config.winrm.password = admin_password
|
||||
communicator_message = 'vagrant_azure.waiting_for_winrm'
|
||||
windows_params = {
|
||||
adminUsername: admin_user_name,
|
||||
adminPassword: admin_password,
|
||||
winRmPort: winrm_port
|
||||
}
|
||||
deployment_params.merge!(windows_params)
|
||||
end
|
||||
|
||||
template_params = {
|
||||
operating_system: get_image_os(image_details)
|
||||
}
|
||||
|
||||
env[:ui].info(" -- Create or Update of Resource Group: #{resource_group_name}")
|
||||
env[:metrics]['put_resource_group'] = Util::Timer.time do
|
||||
put_resource_group(azure, resource_group_name, location)
|
||||
|
@ -114,8 +143,8 @@ module VagrantPlugins
|
|||
@logger.info("Time to deploy: #{env[:metrics]['deployment_time']}")
|
||||
unless env[:interrupted]
|
||||
env[:metrics]['instance_ssh_time'] = Util::Timer.time do
|
||||
# Wait for SSH to be ready.
|
||||
env[:ui].info(I18n.t('vagrant_azure.waiting_for_ssh'))
|
||||
# Wait for SSH/WinRM to be ready.
|
||||
env[:ui].info(I18n.t(communicator_message))
|
||||
network_ready_retries = 0
|
||||
network_ready_retries_max = 10
|
||||
while true
|
||||
|
@ -125,7 +154,7 @@ module VagrantPlugins
|
|||
rescue Exception => e
|
||||
if network_ready_retries < network_ready_retries_max
|
||||
network_ready_retries += 1
|
||||
@logger.warn(I18n.t('vagrant_azure.waiting_for_ssh, retrying'))
|
||||
@logger.warn(I18n.t("#{communicator_message}, retrying"))
|
||||
else
|
||||
raise e
|
||||
end
|
||||
|
@ -134,10 +163,10 @@ module VagrantPlugins
|
|||
end
|
||||
end
|
||||
|
||||
@logger.info("Time for SSH ready: #{env[:metrics]['instance_ssh_time']}")
|
||||
@logger.info("Time for SSH/WinRM ready: #{env[:metrics]['instance_ssh_time']}")
|
||||
|
||||
# Ready and booted!
|
||||
env[:ui].info(I18n.t('vagrant_azure.ready'))
|
||||
env[:ui].info(I18n.t('vagrant_azure.ready')) unless env[:interrupted]
|
||||
end
|
||||
|
||||
# Terminate the instance if we were interrupted
|
||||
|
@ -173,13 +202,25 @@ module VagrantPlugins
|
|||
|
||||
# This method generates the deployment template
|
||||
def render_deployment_template(options)
|
||||
Vagrant::Util::TemplateRenderer.render('arm/deployment.json', options.merge(template_root: template_root))
|
||||
if options[:operating_system] == 'Windows' && options[:winrm_install_self_signed_cert]
|
||||
setup_winrm_powershell = Vagrant::Util::TemplateRenderer.render('arm/setup-winrm.ps1', options.merge({template_root: template_root}))
|
||||
encoded_setup_winrm_powershell = setup_winrm_powershell.
|
||||
gsub("'", "', variables('singleQuote'), '").
|
||||
gsub("\r\n", "\n").
|
||||
gsub("\n", "; ")
|
||||
self_signed_cert_resource = Vagrant::Util::TemplateRenderer.render('arm/selfsignedcert.json', options.merge({template_root: template_root, setup_winrm_powershell: encoded_setup_winrm_powershell}))
|
||||
end
|
||||
Vagrant::Util::TemplateRenderer.render('arm/deployment.json', options.merge({ template_root: template_root, self_signed_cert_resource: self_signed_cert_resource}))
|
||||
end
|
||||
|
||||
def build_deployment_params(template_params, deployment_params)
|
||||
params = ::Azure::ARM::Resources::Models::Deployment.new
|
||||
params.properties = ::Azure::ARM::Resources::Models::DeploymentProperties.new
|
||||
params.properties.template = JSON.parse(render_deployment_template(template_params))
|
||||
if (template_params[:deployment_template].nil?)
|
||||
params.properties.template = JSON.parse(render_deployment_template(template_params))
|
||||
else
|
||||
params.properties.template = JSON.parse(template_params[:deployment_template])
|
||||
end
|
||||
params.properties.mode = ::Azure::ARM::Resources::Models::DeploymentMode::Incremental
|
||||
params.properties.parameters = build_parameters(deployment_params)
|
||||
params
|
||||
|
|
|
@ -20,7 +20,9 @@ module VagrantPlugins
|
|||
|
||||
begin
|
||||
env[:ui].info(I18n.t('vagrant_azure.terminating', parsed))
|
||||
env[:ui].info('Deleting resource group')
|
||||
env[:azure_arm_service].resources.resource_groups.delete(parsed[:group]).value!.body
|
||||
env[:ui].info('Resource group deleted...')
|
||||
rescue MsRestAzure::AzureOperationError => ex
|
||||
unless ex.response.status == 404
|
||||
raise ex
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
module VagrantPlugins
|
||||
module Azure
|
||||
module Cap
|
||||
class WinRM
|
||||
def self.winrm_info(machine)
|
||||
env = machine.action('read_winrm_info')
|
||||
env[:machine_winrm_info]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -93,6 +93,26 @@ module VagrantPlugins
|
|||
# @return [String]
|
||||
attr_accessor :endpoint
|
||||
|
||||
# (Optional - requrired for Windows) The admin username for Windows templates -- ENV['AZURE_VM_ADMIN_USERNAME']
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :admin_username
|
||||
|
||||
# (Optional - Required for Windows) The admin username for Windows templates -- ENV['AZURE_VM_ADMIN_PASSWORD']
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :admin_password
|
||||
|
||||
# (Optional) Whether to automatically install a self-signed cert and open the firewall port for winrm over https -- default true
|
||||
#
|
||||
# @return [Bool]
|
||||
attr_accessor :winrm_install_self_signed_cert
|
||||
|
||||
# (Optional - Required for Windows) The admin username for Windows templates -- ENV['AZURE_VM_ADMIN_PASSWORD']
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :deployment_template
|
||||
|
||||
def initialize
|
||||
@tenant_id = UNSET_VALUE
|
||||
@client_id = UNSET_VALUE
|
||||
|
@ -111,6 +131,10 @@ module VagrantPlugins
|
|||
@availability_set_name = UNSET_VALUE
|
||||
@instance_ready_timeout = UNSET_VALUE
|
||||
@instance_check_interval = UNSET_VALUE
|
||||
@admin_username = UNSET_VALUE
|
||||
@admin_password = UNSET_VALUE
|
||||
@winrm_install_self_signed_cert = UNSET_VALUE
|
||||
@deployment_template = UNSET_VALUE
|
||||
end
|
||||
|
||||
def finalize!
|
||||
|
@ -133,6 +157,11 @@ module VagrantPlugins
|
|||
|
||||
@instance_ready_timeout = 120 if @instance_ready_timeout == UNSET_VALUE
|
||||
@instance_check_interval = 2 if @instance_check_interval == UNSET_VALUE
|
||||
|
||||
@admin_username = ENV['AZURE_VM_ADMIN_USERNAME'] if @admin_username == UNSET_VALUE
|
||||
@admin_password = ENV['AZURE_VM_ADMIN_PASSWORD'] if @admin_password == UNSET_VALUE
|
||||
@winrm_install_self_signed_cert = true if @winrm_install_self_signed_cert == UNSET_VALUE
|
||||
@deployment_template = nil if @deployment_template == UNSET_VALUE
|
||||
end
|
||||
|
||||
def validate(machine)
|
||||
|
|
|
@ -37,6 +37,11 @@ module VagrantPlugins
|
|||
Provider
|
||||
end
|
||||
|
||||
provider_capability(:azure, :winrm_info) do
|
||||
require_relative 'capabilities/winrm'
|
||||
VagrantPlugins::Azure::Cap::WinRM
|
||||
end
|
||||
|
||||
def self.setup_i18n
|
||||
I18n.load_path << File.expand_path(
|
||||
'locales/en.yml',
|
||||
|
|
|
@ -10,6 +10,12 @@ module VagrantPlugins
|
|||
|
||||
def initialize(machine)
|
||||
@machine = machine
|
||||
|
||||
# Load the driver
|
||||
machine_id_changed
|
||||
|
||||
@machine.config.winrm.password = @machine.provider_config.admin_password
|
||||
@machine.config.winrm.username = @machine.provider_config.admin_username
|
||||
end
|
||||
|
||||
def action(name)
|
||||
|
@ -29,6 +35,11 @@ module VagrantPlugins
|
|||
env[:machine_ssh_info]
|
||||
end
|
||||
|
||||
def winrm_info
|
||||
env = @machine.action('read_winrm_info')
|
||||
env[:machine_winrm_info]
|
||||
end
|
||||
|
||||
def state
|
||||
# Run a custom action we define called "read_state" which does what it
|
||||
# says. It puts the state in the `:machine_state_id` key in the env
|
||||
|
|
|
@ -92,6 +92,8 @@ en:
|
|||
you can run `vagrant destroy`.
|
||||
waiting_for_ssh: |-
|
||||
Waiting for SSH to become available...
|
||||
waiting_for_winrm: |-
|
||||
Waiting for WinRM to become available...
|
||||
ready: |-
|
||||
Machine is booted and ready for use!
|
||||
public_key_path_private_key: |-
|
||||
|
|
|
@ -11,18 +11,19 @@
|
|||
},
|
||||
<% if operating_system == 'Windows' %>
|
||||
"adminPassword": {
|
||||
"type": "string",
|
||||
"type": "securestring",
|
||||
"metadata": {
|
||||
"description": "Password for the Virtual Machine (only used on Windows)"
|
||||
}
|
||||
},
|
||||
<% end %>
|
||||
<% else %>
|
||||
"sshKeyData": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
"description": "SSH rsa public key file as a string."
|
||||
}
|
||||
},
|
||||
<% end %>
|
||||
"dnsLabelPrefix": {
|
||||
"type": "string",
|
||||
"metadata": {
|
||||
|
@ -83,6 +84,13 @@
|
|||
"metadata": {
|
||||
"description": "Name of the virtual network"
|
||||
}
|
||||
},
|
||||
"winRmPort": {
|
||||
"type": "int",
|
||||
"defaultValue": 5986,
|
||||
"metadata": {
|
||||
"description": "WinRM port"
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
|
@ -100,7 +108,9 @@
|
|||
"sshKeyPath": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
|
||||
"vnetID": "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]",
|
||||
"subnetRef": "[concat(variables('vnetID'),'/subnets/',parameters('subnetName'))]",
|
||||
"apiVersion": "2015-06-15"
|
||||
"apiVersion": "2015-06-15",
|
||||
"singleQuote": "'",
|
||||
"doubleQuote": "\""
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
|
@ -119,6 +129,36 @@
|
|||
"location": "[variables('location')]",
|
||||
"properties": {
|
||||
"securityRules": [
|
||||
<% if operating_system == 'Windows' %>
|
||||
{
|
||||
"name": "rdp_rule",
|
||||
"properties": {
|
||||
"description": "Enable Inbound RDP",
|
||||
"protocol": "Tcp",
|
||||
"sourcePortRange": "*",
|
||||
"destinationPortRange": "3389",
|
||||
"sourceAddressPrefix": "*",
|
||||
"destinationAddressPrefix": "*",
|
||||
"access": "Allow",
|
||||
"priority": 123,
|
||||
"direction": "Inbound"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "winrm_rule",
|
||||
"properties": {
|
||||
"description": "Enable Inbound WinRM",
|
||||
"protocol": "Tcp",
|
||||
"sourcePortRange": "*",
|
||||
"destinationPortRange": "[parameters('winRmPort')]",
|
||||
"sourceAddressPrefix": "*",
|
||||
"destinationAddressPrefix": "*",
|
||||
"access": "Allow",
|
||||
"priority": 124,
|
||||
"direction": "Inbound"
|
||||
}
|
||||
}
|
||||
<% else %>
|
||||
{
|
||||
"name": "ssh_rule",
|
||||
"properties": {
|
||||
|
@ -133,6 +173,7 @@
|
|||
"direction": "Inbound"
|
||||
}
|
||||
}
|
||||
<% end %>
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -210,6 +251,9 @@
|
|||
"[concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName'))]",
|
||||
"[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
|
||||
],
|
||||
<% if operating_system == 'Windows' %>
|
||||
<%= self_signed_cert_resource %>
|
||||
<% end %>
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"vmSize": "[parameters('vmSize')]"
|
||||
|
@ -217,6 +261,9 @@
|
|||
"osProfile": {
|
||||
"computerName": "[parameters('vmName')]",
|
||||
"adminUsername": "[parameters('adminUsername')]",
|
||||
<% if operating_system == 'Windows' %>
|
||||
"adminPassword": "[parameters('adminPassword')]"
|
||||
<% else %>
|
||||
"linuxConfiguration": {
|
||||
"disablePasswordAuthentication": "true",
|
||||
"ssh": {
|
||||
|
@ -228,6 +275,7 @@
|
|||
]
|
||||
}
|
||||
}
|
||||
<% end %>
|
||||
},
|
||||
"storageProfile": {
|
||||
"imageReference": {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Compute/virtualMachines/extensions",
|
||||
"name": "[concat(parameters('vmName'),'/WinRMCustomScriptExtension')]",
|
||||
"apiVersion": "[variables('apiVersion')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[concat('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"publisher": "Microsoft.Compute",
|
||||
"type": "CustomScriptExtension",
|
||||
"typeHandlerVersion": "1.4",
|
||||
"settings": {
|
||||
"commandToExecute": "[concat('powershell -ExecutionPolicy Unrestricted -command ', variables('doubleQuote'), '& { <%= setup_winrm_powershell %> }', variables('doubleQuote'))]"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
|
@ -0,0 +1,7 @@
|
|||
$hostname = '<%= dns_label_prefix %>.<%= location %>.cloudapp.azure.com'
|
||||
$Cert = (New-SelfSignedCertificate -CertstoreLocation Cert:/LocalMachine/My -DnsName $hostname).Thumbprint
|
||||
$transport = New-Item -Path WSMan:/LocalHost/Listener -Transport HTTPS -Address * -CertificateThumbPrint $Cert -Force
|
||||
cd $transport.PSPath
|
||||
set-item ./HostName -value $hostname -force
|
||||
set-item ./Port -value <%= winrm_port %> -force
|
||||
netsh advfirewall firewall add rule name=WinRM_HTTPS dir=in action=allow protocol=TCP localport=<%= winrm_port %>
|
Загрузка…
Ссылка в новой задаче