Skip to content

Commit 272f05e

Browse files
committed
Merge pull request #129 from hyonholee/crp2
Extension cmdlets, OSProflie cmdlets.
2 parents 8cbdb07 + 0ecb84b commit 272f05e

31 files changed

+1884
-26
lines changed

src/ResourceManager/Compute/Commands.Compute.Test/ScenarioTests/VirtualMachineExtensionTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,21 @@ public void TestVirtualMachineExtension()
2525
{
2626
ComputeTestController.NewInstance.RunPsTest("Test-VirtualMachineExtension");
2727
}
28+
29+
[Fact
30+
(Skip = "Disable this test until the breaking api is checked in.")]
31+
[Trait(Category.AcceptanceType, Category.CheckIn)]
32+
public void TestVirtualMachineCustomScriptExtension()
33+
{
34+
ComputeTestController.NewInstance.RunPsTest("Test-VirtualMachineCustomScriptExtension");
35+
}
36+
37+
[Fact
38+
(Skip = "Disable this test until the breaking api is checked in.")]
39+
[Trait(Category.AcceptanceType, Category.CheckIn)] // disable this test until the breaking api is checked in.
40+
public void TestVirtualMachineAccessExtension()
41+
{
42+
ComputeTestController.NewInstance.RunPsTest("Test-VirtualMachineAccessExtension");
43+
}
2844
}
2945
}

src/ResourceManager/Compute/Commands.Compute.Test/ScenarioTests/VirtualMachineExtensionTests.ps1

Lines changed: 313 additions & 7 deletions
Large diffs are not rendered by default.

src/ResourceManager/Compute/Commands.Compute.Test/ScenarioTests/VirtualMachineProfileTests.ps1

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,87 @@ function Test-VirtualMachineProfile
6666
Assert-AreEqual $p.StorageProfile.DataDisks[1].Lun 1;
6767
Assert-AreEqual $p.StorageProfile.DataDisks[1].VirtualHardDisk.Uri $dataDiskVhdUri2;
6868

69-
# OS
69+
# Windows OS
7070
$user = "Foo12";
7171
$password = 'BaR@000' + ((Get-Random) % 10000);
7272
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force;
7373
$cred = New-Object System.Management.Automation.PSCredential ($user, $securePassword);
7474
$computerName = 'test';
7575
$vhdContainer = "https://$stoname.blob.core.windows.net/test";
7676
$img = 'a699494373c04fc0bc8f2bb1389d6106__Windows-Server-2012-Datacenter-201503.01-en.us-127GB.vhd';
77-
78-
$p = Set-AzureVMOperatingSystem -VM $p -Windows -ComputerName $computerName -Credential $cred;
77+
78+
$referenceUri = "/subscriptions/05cacd0c-6f9b-492e-b673-d8be41a7644f/resourceGroups/RgTest1/providers/Microsoft.KeyVault/vaults/TestVault123";
79+
$certStore = "My";
80+
$certUrl = "https://testvault123.vault.azure.net/secrets/Test1/514ceb769c984379a7e0230bdd703272";
81+
$vaultCert = New-AzureVaultCertificate -CertificateStore $certStore -CertificateUrl $certUrl;
82+
$vaultSG = New-AzureVaultSecretGroup -ReferenceUri $referenceUri -VaultCertificates $vaultCert;
83+
84+
$aucSetting = "AutoLogon";
85+
$aucContent = "<UserAccounts><AdministratorPassword><Value>p@ssw0rd</Value><PlainText>true</PlainText></AdministratorPassword></UserAccounts>";
86+
$auc1 = New-AzureAdditionalUnattendContent -Content $aucContent -SettingName $aucSetting;
87+
$auc2 = New-AzureAdditionalUnattendContent -Content $aucContent -SettingName $aucSetting;
88+
89+
$winRMCertUrl = "http://keyVaultName.vault.azure.net/secrets/secretName/secretVersion";
90+
$timeZone = "Pacific Standard Time";
91+
$custom = "echo 'Hello World'";
92+
$encodedCustom = "ZWNobyAnSGVsbG8gV29ybGQn";
93+
94+
$p = Set-AzureVMOperatingSystem -VM $p -Windows -ComputerName $computerName -Credential $cred -CustomData $custom -Secrets $vaultSG -WinRMHttp -WinRMHttps -WinRMCertUrl $winRMCertUrl -ProvisionVMAgent -EnableAutoUpdate -TimeZone $timeZone -AdditionalUnattendContents $auc1,$auc2;
7995
$p = Set-AzureVMSourceImage -VM $p -Name $img -DestinationVhdsContainer $vhdContainer;
8096

8197
Assert-AreEqual $p.OSProfile.AdminUsername $user;
8298
Assert-AreEqual $p.OSProfile.ComputerName $computerName;
8399
Assert-AreEqual $p.OSProfile.AdminPassword $password;
84100
Assert-AreEqual $p.StorageProfile.DestinationVhdsContainer.ToString() $vhdContainer;
85101
Assert-AreEqual $p.StorageProfile.SourceImage.ReferenceUri ('/' + (Get-AzureSubscription -Current).SubscriptionId + '/services/images/' + $img);
102+
Assert-AreEqual $p.OSProfile.Secrets[0].SourceVault.ReferenceUri $referenceUri;
103+
Assert-AreEqual $p.OSProfile.Secrets[0].VaultCertificates[0].CertificateStore $certStore;
104+
Assert-AreEqual $p.OSProfile.Secrets[0].VaultCertificates[0].CertificateUrl $certUrl;
105+
Assert-AreEqual $encodedCustom $p.OSProfile.CustomData;
106+
107+
# Verify WinRM
108+
Assert-Null $p.OSProfile.WindowsConfiguration.WinRMConfiguration.Listeners[0].CertificateUrl;
109+
Assert-AreEqual "http" $p.OSProfile.WindowsConfiguration.WinRMConfiguration.Listeners[0].Protocol ;
110+
Assert-AreEqual $winRMCertUrl $p.OSProfile.WindowsConfiguration.WinRMConfiguration.Listeners[1].CertificateUrl ;
111+
Assert-AreEqual "https" $p.OSProfile.WindowsConfiguration.WinRMConfiguration.Listeners[1].Protocol ;
112+
113+
# Verify Windows Provisioning Setup
114+
Assert-AreEqual $true $p.OSProfile.WindowsConfiguration.ProvisionVMAgent;
115+
Assert-AreEqual $true $p.OSProfile.WindowsConfiguration.EnableAutomaticUpdates;
116+
Assert-AreEqual $timeZone $p.OSProfile.WindowsConfiguration.TimeZone;
117+
118+
# Verify Additional Unattend Content
119+
Assert-AreEqual "Microsoft-Windows-Shell-Setup" $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[0].ComponentName;
120+
Assert-AreEqual $aucContent $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[0].Content;
121+
Assert-AreEqual "oobeSystem" $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[0].PassName;
122+
Assert-AreEqual $aucSetting $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[0].SettingName;
123+
Assert-AreEqual "Microsoft-Windows-Shell-Setup" $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[1].ComponentName;
124+
Assert-AreEqual $aucContent $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[1].Content;
125+
Assert-AreEqual "oobeSystem" $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[1].PassName;
126+
Assert-AreEqual $aucSetting $p.OSProfile.WindowsConfiguration.AdditionalUnattendContents[1].SettingName;
127+
128+
# Linux OS
129+
$img = "b4590d9e3ed742e4a1d46e5424aa335e__SUSE-Linux-Enterprise-Server-11-SP3-v206";
130+
$sshPath = "/home/pstestuser/.ssh/authorized_keys";
131+
$sshPublicKey = "MIIDszCCApugAwIBAgIJALBV9YJCF/tAMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV";
132+
133+
$sshKey = New-AzureSshPublicKey -KeyData $sshPublicKey -Path $sshPath;
134+
135+
$p = Set-AzureVMOperatingSystem -VM $p -Linux -ComputerName $computerName -Credential $cred -CustomData $custom -Secrets $vaultSG -SSHPublicKeys $sshKey -DisablePasswordAuthentication;
136+
$p = Set-AzureVMSourceImage -VM $p -Name $img -DestinationVhdsContainer $vhdContainer;
137+
138+
Assert-AreEqual $p.OSProfile.AdminUsername $user;
139+
Assert-AreEqual $p.OSProfile.ComputerName $computerName;
140+
Assert-AreEqual $p.OSProfile.AdminPassword $password;
141+
Assert-AreEqual $p.StorageProfile.DestinationVhdsContainer.ToString() $vhdContainer;
142+
Assert-AreEqual $p.StorageProfile.SourceImage.ReferenceUri ('/' + (Get-AzureSubscription -Current).SubscriptionId + '/services/images/' + $img);
143+
Assert-AreEqual $p.OSProfile.Secrets[0].SourceVault.ReferenceUri $referenceUri;
144+
Assert-AreEqual $p.OSProfile.Secrets[0].VaultCertificates[0].CertificateStore $certStore;
145+
Assert-AreEqual $p.OSProfile.Secrets[0].VaultCertificates[0].CertificateUrl $certUrl;
146+
Assert-AreEqual $encodedCustom $p.OSProfile.CustomData;
147+
148+
# Verify SSH configuration
149+
Assert-AreEqual $sshPublicKey $p.OSProfile.LinuxConfiguration.SshConfiguration.PublicKeys[0].KeyData;
150+
Assert-AreEqual $sshPath $p.OSProfile.LinuxConfiguration.SshConfiguration.PublicKeys[0].Path;
151+
Assert-AreEqual $true $p.OSProfile.LinuxConfiguration.DisablePasswordAuthentication
86152
}

src/ResourceManager/Compute/Commands.Compute/Commands.Compute.csproj

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
<SpecificVersion>False</SpecificVersion>
104104
<HintPath>..\..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll</HintPath>
105105
</Reference>
106+
<Reference Include="Microsoft.WindowsAzure.Storage, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
107+
<SpecificVersion>False</SpecificVersion>
108+
<HintPath>..\..\..\packages\WindowsAzure.Storage.4.0.0\lib\net40\Microsoft.WindowsAzure.Storage.dll</HintPath>
109+
</Reference>
106110
<Reference Include="Newtonsoft.Json">
107111
<HintPath>..\..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
108112
<Private>True</Private>
@@ -133,20 +137,35 @@
133137
<Compile Include="AvailabilitySets\GetAzureAvailabilitySetCommand.cs" />
134138
<Compile Include="AvailabilitySets\NewAzureAvailabilitySetCommand.cs" />
135139
<Compile Include="AvailabilitySets\AvailabilitySetBaseCmdlet.cs" />
136-
<Compile Include="Common\ExtensionSettingBuilder.cs" />
137140
<Compile Include="Common\ComputeClient.cs" />
138141
<Compile Include="ExtensionImages\GetAzureVMExtensionImageTypeCommand.cs" />
139142
<Compile Include="ExtensionImages\GetAzureVMExtensionImageVersionCommand.cs" />
140143
<Compile Include="ExtensionImages\GetAzureVMExtensionImageCommand.cs" />
141144
<Compile Include="ExtensionImages\VirtualMachineExtensionImageBaseCmdlet.cs" />
145+
<Compile Include="Extension\CustomScript\GetAzureVMCustomScriptExtensionCommand.cs" />
146+
<Compile Include="Extension\CustomScript\CustomScriptExtensionPrivateSettings.cs" />
147+
<Compile Include="Extension\CustomScript\CustomScriptExtensionPublicSettings.cs" />
148+
<Compile Include="Extension\CustomScript\RemoveAzureVMCustomScriptExtensionCommand.cs" />
149+
<Compile Include="Extension\CustomScript\SetAzureVMCustomScriptExtensionCommand.cs" />
150+
<Compile Include="Extension\CustomScript\VirtualMachineCustomScriptExtensionContext.cs" />
142151
<Compile Include="Extension\SetAzureVMExtensionCommand.cs" />
143152
<Compile Include="Extension\RemoveAzureVMExtensionCommand.cs" />
144153
<Compile Include="Extension\GetAzureVMExtensionCommand.cs" />
154+
<Compile Include="Extension\VMAccess\GetAzureVMAccessExtension.cs" />
155+
<Compile Include="Extension\VMAccess\RemoveAzureVMAccessExtension.cs" />
156+
<Compile Include="Extension\VMAccess\SetAzureVMAccessExtension.cs" />
157+
<Compile Include="Extension\VMAccess\VirtualMachineAccessExtensionContext.cs" />
158+
<Compile Include="Extension\VMAccess\VMAccessExtensionPrivateSettings.cs" />
159+
<Compile Include="Extension\VMAccess\VMAccessExtensionPublicSettings.cs" />
145160
<Compile Include="Images\GetAzureVMImageSkuCommand.cs" />
146161
<Compile Include="Images\GetAzureVMImagePublisherCommand.cs" />
147162
<Compile Include="Images\GetAzureVMImageOfferCommand.cs" />
148163
<Compile Include="Images\GetAzureVMImageCommand.cs" />
149164
<Compile Include="Images\VirtualMachineImageBaseCmdlet.cs" />
165+
<Compile Include="Models\PSAdditionalUnattendContent.cs" />
166+
<Compile Include="Models\PSSshPublicKey.cs" />
167+
<Compile Include="Models\PSVaultCertificate.cs" />
168+
<Compile Include="Models\PSVaultSecretGroup.cs" />
150169
<Compile Include="Models\PSVirtualMachineExtension.cs" />
151170
<Compile Include="Models\PSAvailabilitySet.cs" />
152171
<Compile Include="Models\PSVirtualMachineInstanceView.cs" />
@@ -165,6 +184,10 @@
165184
<Compile Include="VirtualMachineSizes\VirtualMachineSizeBaseCmdlet.cs" />
166185
<Compile Include="VirtualMachine\Action\SaveAzureVMImageCommand.cs" />
167186
<Compile Include="VirtualMachine\Action\SetAzureVMCommand.cs" />
187+
<Compile Include="VirtualMachine\Config\NewAzureAdditionalUnattendContentCommand.cs" />
188+
<Compile Include="VirtualMachine\Config\NewAzureSshPublicKeyCommand.cs" />
189+
<Compile Include="VirtualMachine\Config\NewAzureVaultCertificateCommand.cs" />
190+
<Compile Include="VirtualMachine\Config\NewAzureVaultSecretGroupCommand.cs" />
168191
<Compile Include="VirtualMachine\Config\RemoveAzureVMNetworkInterfaceCommand.cs" />
169192
<Compile Include="VirtualMachine\Config\RemoveAzureVMDataDiskCommand.cs" />
170193
<Compile Include="VirtualMachine\Config\SetAzureVMSourceImage.cs" />

src/ResourceManager/Compute/Commands.Compute/Common/ConstantStringTypes.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public static class ProfileNouns
6565

6666
public const string VirtualMachine = "AzureVM";
6767
public const string VirtualMachineExtension = "AzureVMExtension";
68+
public const string VirtualMachineCustomScriptExtension = "AzureVMCustomScriptExtension";
69+
public const string VirtualMachineAccessExtension = "AzureVMAccessExtension";
6870
public const string VirtualMachineExtensionImage = "AzureVMExtensionImage";
6971
public const string VirtualMachineExtensionImageVersion = "AzureVMExtensionImageVersion";
7072
public const string VirtualMachineExtensionImageType = "AzureVMExtensionImageType";
@@ -80,5 +82,10 @@ public static class ProfileNouns
8082
public const string VirtualMachineImageSku = "AzureVMImageSku";
8183

8284
public const string VirtualMachineUsage = "AzureVMUsage";
85+
86+
public const string SshPublicKey = "AzureSshPublicKey";
87+
public const string AdditionalUnattendContent = "AzureAdditionalUnattendContent";
88+
public const string VaultCertificate = "AzureVaultCertificate";
89+
public const string VaultSecretGroup = "AzureVaultSecretGroup";
8390
}
8491
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
namespace Microsoft.Azure.Commands.Compute
16+
{
17+
public class CustomScriptExtensionPrivateSettings
18+
{
19+
public string storageAccountName;
20+
public string storageAccountKey;
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
namespace Microsoft.Azure.Commands.Compute
16+
{
17+
public class CustomScriptExtensionPublicSettings
18+
{
19+
public string[] fileUris;
20+
public string commandToExecute;
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using Microsoft.Azure.Commands.Compute.Common;
16+
using Microsoft.Azure.Commands.Compute.Models;
17+
using Microsoft.Azure.Management.Compute;
18+
using Newtonsoft.Json;
19+
using System;
20+
using System.Management.Automation;
21+
22+
namespace Microsoft.Azure.Commands.Compute
23+
{
24+
[Cmdlet(
25+
VerbsCommon.Get,
26+
ProfileNouns.VirtualMachineCustomScriptExtension,
27+
DefaultParameterSetName = GetCustomScriptExtensionParamSetName),
28+
OutputType(
29+
typeof(VirtualMachineCustomScriptExtensionContext))]
30+
public class GetAzureVMCustomScriptExtensionCommand : VirtualMachineExtensionBaseCmdlet
31+
{
32+
protected const string GetCustomScriptExtensionParamSetName = "GetCustomScriptExtension";
33+
34+
[Parameter(
35+
Position = 3,
36+
ValueFromPipelineByPropertyName = true,
37+
HelpMessage = "To show the status.")]
38+
[ValidateNotNullOrEmpty]
39+
public SwitchParameter Status { get; set; }
40+
41+
public override void ExecuteCmdlet()
42+
{
43+
base.ExecuteCmdlet();
44+
45+
if (Status)
46+
{
47+
var result = this.VirtualMachineExtensionClient.GetWithInstanceView(this.ResourceGroupName, this.VMName, this.Name);
48+
var returnedExtension = result.ToPSVirtualMachineExtension(this.ResourceGroupName);
49+
50+
if (returnedExtension.Publisher.Equals(VirtualMachineCustomScriptExtensionContext.ExtensionDefaultPublisher, StringComparison.InvariantCultureIgnoreCase) &&
51+
returnedExtension.ExtensionType.Equals(VirtualMachineCustomScriptExtensionContext.ExtensionDefaultName, StringComparison.InvariantCultureIgnoreCase))
52+
{
53+
WriteObject(new VirtualMachineCustomScriptExtensionContext(returnedExtension));
54+
}
55+
else
56+
{
57+
WriteObject(null);
58+
}
59+
}
60+
else
61+
{
62+
var result = this.VirtualMachineExtensionClient.Get(this.ResourceGroupName, this.VMName, this.Name);
63+
var returnedExtension = result.ToPSVirtualMachineExtension(this.ResourceGroupName);
64+
65+
if (returnedExtension.Publisher.Equals(VirtualMachineCustomScriptExtensionContext.ExtensionDefaultPublisher, StringComparison.InvariantCultureIgnoreCase) &&
66+
returnedExtension.ExtensionType.Equals(VirtualMachineCustomScriptExtensionContext.ExtensionDefaultName, StringComparison.InvariantCultureIgnoreCase))
67+
{
68+
WriteObject(new VirtualMachineCustomScriptExtensionContext(returnedExtension));
69+
}
70+
else
71+
{
72+
WriteObject(null);
73+
}
74+
}
75+
}
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
15+
using Microsoft.Azure.Commands.Compute.Common;
16+
using Microsoft.Azure.Management.Compute;
17+
using System.Management.Automation;
18+
19+
namespace Microsoft.Azure.Commands.Compute
20+
{
21+
[Cmdlet(
22+
VerbsCommon.Remove,
23+
ProfileNouns.VirtualMachineCustomScriptExtension)]
24+
public class RemoveAzureVMCustomScriptExtensionCommand : VirtualMachineExtensionBaseCmdlet
25+
{
26+
27+
[Parameter(HelpMessage = "To force the removal.")]
28+
[ValidateNotNullOrEmpty]
29+
public SwitchParameter Force { get; set; }
30+
31+
public override void ExecuteCmdlet()
32+
{
33+
base.ExecuteCmdlet();
34+
35+
if (this.Force.IsPresent
36+
|| this.ShouldContinue(Properties.Resources.VirtualMachineExtensionRemovalConfirmation, Properties.Resources.VirtualMachineExtensionRemovalCaption))
37+
{
38+
var op = this.VirtualMachineExtensionClient.Delete(this.ResourceGroupName, this.VMName, this.Name);
39+
WriteObject(op);
40+
}
41+
}
42+
43+
}
44+
}

0 commit comments

Comments
 (0)