mirror of https://github.com/apache/jclouds.git
Merge pull request #834 from andreaturli/virtualbox
issue 384: added support for ubuntu12.04; prepared for supporting remote host
This commit is contained in:
commit
80f6101e1f
|
@ -1,9 +1,17 @@
|
|||
|
||||
#Setup
|
||||
|
||||
Have virtualbox 4.1.8 installed.
|
||||
Have virtualbox 4.1.20r80170 installed.
|
||||
|
||||
Have an ssh daemon with passwordless login to localhost (i.e. "ssh [me]@localhost" must work without password).
|
||||
To achieve that, be sure you have your ssh public key (at System.getProperty("user.home") + "/.ssh/id_rsa") in your '.ssh/authorized_keys'.
|
||||
Please look at [this example]http://www.linuxproblem.org/art_9.html for more details.
|
||||
|
||||
You can have also specify '-Dvirtualbox.identity' and '-Dvirtualbox.credential' if you want to use a username and password of your local machine.
|
||||
|
||||
In order to make available a preseed file, jclouds-vbox will start a PreseedServer at `http://localhost:23232` that will serve a preseed.cfg file.
|
||||
Make sure your firewall rules are not blocking this port.
|
||||
If you need to override this default you can use -Djclouds.virtualbox.preconfigurationurl=http://localhost:PORT/preseed.cfg, with a different PORT.
|
||||
|
||||
That's it!
|
||||
|
||||
|
@ -17,7 +25,7 @@ Enjoy local cloud goodness by running:
|
|||
|
||||
> (use 'org.jclouds.compute2)
|
||||
> (import 'org.jclouds.scriptbuilder.statements.login.AdminAccess)
|
||||
> (def compute (compute-service "virtualbox" "admin" "12345" :sshj :slf4j))
|
||||
> (def compute (compute-service "virtualbox" "user" "password" :sshj :slf4j))
|
||||
> (create-nodes compute "local-cluster" 2 (build-template compute { :run-script (AdminAccess/standard) } ))
|
||||
|
||||
--------------
|
||||
|
@ -39,7 +47,7 @@ It *should* behave as any other provider, if not please report.
|
|||
|
||||
- jclouds-vbox is still at alpha stage please report any issues you find.
|
||||
- jclouds-vbox has been mostly tested on Mac OSX, it might work on Linux iff vbox is running and correctly set up, but it won't work on windows for the moment.
|
||||
- cached isos, vm's and most configs are kept at ~/.jclouds-vbox/ by default.
|
||||
- cached isos, vm's and most configs are kept at ~/.jclouds-vbox/ by default, you can override -Dtest.virtualbox.workingDir=/path/to/your/workingDir.
|
||||
- jclouds-vbox assumes vbox has the default host-only network vboxnet0, that the network is in 192.168.86.0/255.255.255.0 and that the host has address 1 in this network.
|
||||
|
||||
--------------
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
<properties>
|
||||
<test.virtualbox.endpoint>http://localhost:18083/</test.virtualbox.endpoint>
|
||||
<test.virtualbox.api-version>4.1.4</test.virtualbox.api-version>
|
||||
<test.virtualbox.build-version>4.1.8r75467</test.virtualbox.build-version>
|
||||
<test.virtualbox.build-version>4.1.20r80170</test.virtualbox.build-version>
|
||||
<test.virtualbox.identity>administrator</test.virtualbox.identity>
|
||||
<test.virtualbox.credential>12345</test.virtualbox.credential>
|
||||
<test.virtualbox.template>imageId=test-ubuntu-11.10-i386,loginUser=toor:password,authenticateSudo=true</test.virtualbox.template>
|
||||
<test.virtualbox.credential>CHANGE_ME</test.virtualbox.credential>
|
||||
<test.virtualbox.template>osFamily=UBUNTU,osVersionMatches=12.04.1,os64Bit=true,osArchMatches=amd64,loginUser=toor:password,authenticateSudo=true</test.virtualbox.template>
|
||||
<jclouds.osgi.export>org.jclouds.virtualbox*;version="${project.version}"</jclouds.osgi.export>
|
||||
<jclouds.osgi.import>org.jclouds*;version="${project.version}",*</jclouds.osgi.import>
|
||||
</properties>
|
||||
|
@ -136,6 +136,7 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<threadCount>1</threadCount>
|
||||
<parallel>false</parallel>
|
||||
<systemPropertyVariables>
|
||||
<test.virtualbox.endpoint>${test.virtualbox.endpoint}</test.virtualbox.endpoint>
|
||||
<test.virtualbox.api-version>${test.virtualbox.api-version}</test.virtualbox.api-version>
|
||||
|
|
|
@ -20,6 +20,8 @@ package org.jclouds.virtualbox;
|
|||
|
||||
import static org.jclouds.compute.config.ComputeServiceProperties.TEMPLATE;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_DEFAULT_DIR;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_CREDENTIAL;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_IDENTITY;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_IMAGES_DESCRIPTOR;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_INSTALLATION_KEY_SEQUENCE;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_PRECONFIGURATION_URL;
|
||||
|
@ -81,7 +83,13 @@ public class VirtualBoxApiMetadata extends BaseApiMetadata {
|
|||
properties.put(VIRTUALBOX_IMAGES_DESCRIPTOR, yamlDescriptor);
|
||||
|
||||
properties.put(VIRTUALBOX_PRECONFIGURATION_URL, "http://10.0.2.2:23232/preseed.cfg");
|
||||
properties.setProperty(TEMPLATE, "osFamily=UBUNTU,osVersionMatches=11.10,os64Bit=true,osArchMatches=x86,loginUser=toor:password,authenticateSudo=true");
|
||||
|
||||
properties.put(VIRTUALBOX_GUEST_IDENTITY, "toor");
|
||||
properties.put(VIRTUALBOX_GUEST_CREDENTIAL, "password");
|
||||
properties.setProperty(TEMPLATE,
|
||||
String.format("osFamily=UBUNTU,osVersionMatches=12.04.1,os64Bit=true,osArchMatches=amd64,loginUser=%s:%s,authenticateSudo=true",
|
||||
properties.getProperty(VIRTUALBOX_GUEST_IDENTITY),
|
||||
properties.getProperty(VIRTUALBOX_GUEST_CREDENTIAL)));
|
||||
return properties;
|
||||
}
|
||||
|
||||
|
@ -93,8 +101,8 @@ public class VirtualBoxApiMetadata extends BaseApiMetadata {
|
|||
.identityName("User")
|
||||
.credentialName("Password")
|
||||
.documentation(URI.create("https://www.virtualbox.org/sdkref/index.html"))
|
||||
.defaultIdentity("administrator")
|
||||
.defaultCredential("12345")
|
||||
.defaultIdentity(System.getProperty("user.name"))
|
||||
.defaultCredential("CHANGE_ME")
|
||||
.defaultEndpoint("http://localhost:18083/")
|
||||
.documentation(URI.create("https://github.com/jclouds/jclouds/tree/master/apis/byon"))
|
||||
// later version not in maven, yet
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.jclouds.domain.LocationScope;
|
|||
import org.jclouds.domain.LoginCredentials;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.io.Files;
|
||||
|
@ -79,7 +80,7 @@ import com.google.inject.Provides;
|
|||
public class HardcodeLocalhostAsNodeMetadataSupplier extends AbstractModule {
|
||||
|
||||
public static final String HOST_ID = "host";
|
||||
public static final String HOSTNAME = System.getenv("HOSTNAME");
|
||||
public static final String HOSTNAME = Strings.nullToEmpty(System.getenv("HOSTNAME"));
|
||||
|
||||
/**
|
||||
* Lazy so that we don't hang up the injector reading a file
|
||||
|
@ -91,9 +92,7 @@ public class HardcodeLocalhostAsNodeMetadataSupplier extends AbstractModule {
|
|||
|
||||
@Override
|
||||
public NodeMetadata get() {
|
||||
|
||||
String privateKey = readRsaIdentity();
|
||||
|
||||
return new NodeMetadataBuilder()
|
||||
.id(HOST_ID)
|
||||
.name("host installing virtualbox")
|
||||
|
@ -110,9 +109,9 @@ public class HardcodeLocalhostAsNodeMetadataSupplier extends AbstractModule {
|
|||
.description(HOSTNAME)
|
||||
.build())
|
||||
.credentials(LoginCredentials.builder()
|
||||
.user(System.getProperty("user.name"))
|
||||
.privateKey(privateKey)
|
||||
.build())
|
||||
.user(System.getProperty("user.name"))
|
||||
.privateKey(privateKey)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -150,7 +150,10 @@ public class VirtualBoxComputeServiceContextModule extends
|
|||
|
||||
@Override
|
||||
public VirtualBoxManager apply(Supplier<NodeMetadata> nodeSupplier) {
|
||||
return VirtualBoxManager.createInstance(nodeSupplier.get().getId());
|
||||
if(nodeSupplier.get().getId() != null)
|
||||
return VirtualBoxManager.createInstance(nodeSupplier.get().getId());
|
||||
|
||||
return VirtualBoxManager.createInstance("");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -62,4 +62,7 @@ public interface VirtualBoxConstants {
|
|||
|
||||
public static final String VIRTUALBOX_PROVIDER = "virtualbox";
|
||||
|
||||
public static final String VIRTUALBOX_GUEST_IDENTITY = "jclouds.virtualbox.guest.identity";
|
||||
public static final String VIRTUALBOX_GUEST_CREDENTIAL = "jclouds.virtualbox.guest.credential";
|
||||
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@ public class YamlImage {
|
|||
public String os_family;
|
||||
public String os_description;
|
||||
public String os_version;
|
||||
public String iso_md5;
|
||||
public String iso;
|
||||
public String keystroke_sequence;
|
||||
public String preseed_cfg;
|
||||
|
|
|
@ -21,6 +21,7 @@ package org.jclouds.virtualbox.functions;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Named;
|
||||
|
@ -61,6 +62,8 @@ public class CloneAndRegisterMachineFromIMachineIfNotAlreadyExists implements Fu
|
|||
private final String workingDir;
|
||||
private final MachineUtils machineUtils;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
@Inject
|
||||
public CloneAndRegisterMachineFromIMachineIfNotAlreadyExists(Supplier<VirtualBoxManager> manager,
|
||||
@Named(VirtualBoxConstants.VIRTUALBOX_WORKINGDIR) String workingDir, MachineUtils machineUtils) {
|
||||
|
@ -104,15 +107,13 @@ public class CloneAndRegisterMachineFromIMachineIfNotAlreadyExists implements Fu
|
|||
if (isLinkedClone)
|
||||
options.add(CloneOptions.Link);
|
||||
|
||||
// TODO snapshot name
|
||||
ISnapshot currentSnapshot = new TakeSnapshotIfNotAlreadyAttached(manager, "snapshotName", "snapshotDesc", logger)
|
||||
.apply(master);
|
||||
|
||||
// clone
|
||||
IProgress progress = currentSnapshot.getMachine().cloneTo(clonedMachine, CloneMode.MachineState, options);
|
||||
|
||||
ISnapshot currentSnapshot = new TakeSnapshotIfNotAlreadyAttached(manager,
|
||||
"snapshotName", "snapshotDesc", logger).apply(master);
|
||||
IProgress progress = currentSnapshot.getMachine().cloneTo(clonedMachine,
|
||||
CloneMode.MachineState, options);
|
||||
progress.waitForCompletion(-1);
|
||||
logger.debug(String.format("Machine %s is cloned correctly", clonedMachine.getName()));
|
||||
logger.debug(String.format("Machine %s is cloned correctly",
|
||||
clonedMachine.getName()));
|
||||
|
||||
// memory may not be the same as the master vm
|
||||
clonedMachine.setMemorySize(cloneSpec.getVmSpec().getMemory());
|
||||
|
|
|
@ -42,7 +42,9 @@ import org.jclouds.virtualbox.domain.VmSpec;
|
|||
import org.jclouds.virtualbox.statements.InstallGuestAdditions;
|
||||
import org.jclouds.virtualbox.util.MachineController;
|
||||
import org.jclouds.virtualbox.util.MachineUtils;
|
||||
import org.virtualbox_4_1.DeviceType;
|
||||
import org.virtualbox_4_1.IMachine;
|
||||
import org.virtualbox_4_1.IMediumAttachment;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
|
@ -88,19 +90,14 @@ public class CreateAndInstallVm implements Function<MasterSpec, IMachine> {
|
|||
|
||||
@Override
|
||||
public IMachine apply(MasterSpec masterSpec) {
|
||||
|
||||
VmSpec vmSpec = masterSpec.getVmSpec();
|
||||
IsoSpec isoSpec = masterSpec.getIsoSpec();
|
||||
String vmName = vmSpec.getVmName();
|
||||
|
||||
IMachine vm = createAndRegisterMachineFromIsoIfNotAlreadyExists.apply(masterSpec);
|
||||
|
||||
// Launch machine and wait for it to come online
|
||||
machineController.ensureMachineIsLaunched(vmName);
|
||||
|
||||
String installationKeySequence = isoSpec.getInstallationKeySequence().replace("PRECONFIGURATION_URL",
|
||||
preconfigurationUrl);
|
||||
|
||||
configureOsInstallationWithKeyboardSequence(vmName, installationKeySequence);
|
||||
|
||||
// the OS installation is a long process: let's delay the check for ssh of 30 sec
|
||||
|
@ -111,11 +108,8 @@ public class CreateAndInstallVm implements Function<MasterSpec, IMachine> {
|
|||
}
|
||||
|
||||
SshClient client = sshClientForIMachine.apply(vm);
|
||||
|
||||
logger.debug(">> awaiting installation to finish node(%s)", vmName);
|
||||
|
||||
checkState(sshResponds.apply(client), "timed out waiting for guest %s to be accessible via ssh", vmName);
|
||||
|
||||
NodeMetadata nodeMetadata = imachineToNodeMetadata.apply(vm);
|
||||
|
||||
logger.debug(">> awaiting post-installation actions on vm: %s", vmName);
|
||||
|
@ -129,9 +123,17 @@ public class CreateAndInstallVm implements Function<MasterSpec, IMachine> {
|
|||
new InstallGuestAdditions(vmSpec, version), RunScriptOptions.NONE);
|
||||
ExecResponse gaInstallationResponse = Futures.getUnchecked(execInstallGA);
|
||||
checkState(gaInstallationResponse.getExitStatus() == 0);
|
||||
|
||||
machineController.ensureMachineIsShutdown(vmName);
|
||||
|
||||
Iterable<IMediumAttachment> mediumAttachments = Iterables.filter(vm.getMediumAttachmentsOfController("IDE Controller"),
|
||||
new Predicate<IMediumAttachment>() {
|
||||
public boolean apply(IMediumAttachment in) {
|
||||
return in.getMedium() != null && in.getMedium().getDeviceType().equals(DeviceType.DVD);
|
||||
}
|
||||
});
|
||||
for (IMediumAttachment iMediumAttachment : mediumAttachments) {
|
||||
machineUtils.writeLockMachineAndApply(vm.getName(), new DetachDistroMediumFromMachine(
|
||||
iMediumAttachment.getController(), iMediumAttachment.getPort(), iMediumAttachment.getDevice()));
|
||||
}
|
||||
return vm;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.jclouds.virtualbox.functions;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Named;
|
||||
|
||||
import org.jclouds.compute.domain.NodeMetadata;
|
||||
import org.jclouds.compute.domain.NodeMetadataBuilder;
|
||||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.domain.LoginCredentials;
|
||||
import org.jclouds.javax.annotation.Nullable;
|
||||
import org.jclouds.location.Provider;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.rest.annotations.Credential;
|
||||
import org.jclouds.rest.annotations.Identity;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public class HardcodedHostToHostNodeMetadata implements
|
||||
Function<NodeMetadata, NodeMetadata> {
|
||||
|
||||
@Resource
|
||||
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
|
||||
protected Logger logger = Logger.NULL;
|
||||
|
||||
private final Supplier<URI> providerSupplier;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
@Inject
|
||||
public HardcodedHostToHostNodeMetadata(
|
||||
@Provider Supplier<URI> providerSupplier,
|
||||
@Nullable @Identity String identity,
|
||||
@Nullable @Credential String credential) {
|
||||
this.providerSupplier = checkNotNull(providerSupplier,
|
||||
"endpoint to virtualbox websrvd is needed");
|
||||
this.username = identity;
|
||||
this.password = credential.equals("CHANGE_ME") ? "" : credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeMetadata apply(NodeMetadata host) {
|
||||
|
||||
LoginCredentials.Builder credentialsBuilder = LoginCredentials.builder(
|
||||
host.getCredentials()).user(username);
|
||||
if (!password.isEmpty())
|
||||
credentialsBuilder.password(password);
|
||||
|
||||
return NodeMetadataBuilder
|
||||
.fromNodeMetadata(host)
|
||||
.credentials(credentialsBuilder.build())
|
||||
.publicAddresses(ImmutableList.of(providerSupplier.get().getHost()))
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -46,7 +46,6 @@ import org.virtualbox_4_1.NetworkAttachmentType;
|
|||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
@ -82,7 +81,6 @@ public class IMachineToNodeMetadata implements Function<IMachine, NodeMetadata>
|
|||
|
||||
NodeMetadataBuilder nodeMetadataBuilder = new NodeMetadataBuilder();
|
||||
nodeMetadataBuilder.name(name).ids(vm.getName()).group(group);
|
||||
|
||||
// TODO Set up location properly
|
||||
LocationBuilder locationBuilder = new LocationBuilder();
|
||||
locationBuilder.description("");
|
||||
|
@ -96,41 +94,8 @@ public class IMachineToNodeMetadata implements Function<IMachine, NodeMetadata>
|
|||
if (nodeState == null)
|
||||
nodeState = Status.UNRECOGNIZED;
|
||||
nodeMetadataBuilder.status(nodeState);
|
||||
|
||||
/*
|
||||
// nat adapter
|
||||
INetworkAdapter natAdapter = vm.getNetworkAdapter(0l);
|
||||
checkNotNull(natAdapter, "slot 0 networkadapter");
|
||||
checkState(natAdapter.getAttachmentType() == NetworkAttachmentType.NAT,
|
||||
"expecting slot 0 to be a NAT attachment type (was: " + natAdapter.getAttachmentType() + ")");
|
||||
|
||||
int ipTermination = 0;
|
||||
int inPort = 0;
|
||||
String hostAddress = "";
|
||||
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of(natAdapter.getNatDriver().getHostIP()));
|
||||
for (String nameProtocolnumberAddressInboudportGuestTargetport : natAdapter.getNatDriver().getRedirects()) {
|
||||
Iterable<String> stuff = Splitter.on(',').split(nameProtocolnumberAddressInboudportGuestTargetport);
|
||||
String protocolNumber = Iterables.get(stuff, 1);
|
||||
hostAddress = Iterables.get(stuff, 2);
|
||||
String inboundPort = Iterables.get(stuff, 3);
|
||||
String targetPort = Iterables.get(stuff, 5);
|
||||
if ("1".equals(protocolNumber) && "22".equals(targetPort)) {
|
||||
inPort = Integer.parseInt(inboundPort);
|
||||
ipTermination = inPort % NodeCreator.NODE_PORT_INIT + 2;
|
||||
}
|
||||
}
|
||||
|
||||
// only masters use 2222 port
|
||||
if (inPort == MastersLoadingCache.MASTER_PORT) {
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of(hostAddress)).loginPort(inPort);
|
||||
} else {
|
||||
nodeMetadataBuilder.privateAddresses(ImmutableSet.of((NodeCreator.VMS_NETWORK + ipTermination) + ""));
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of((NodeCreator.VMS_NETWORK + ipTermination) + ""));
|
||||
}
|
||||
*/
|
||||
|
||||
nodeMetadataBuilder = getIpAddresses(vm, nodeMetadataBuilder);
|
||||
|
||||
LoginCredentials loginCredentials = new LoginCredentials("toor", "password", null, true);
|
||||
nodeMetadataBuilder.credentials(loginCredentials);
|
||||
|
||||
|
@ -144,7 +109,9 @@ public class IMachineToNodeMetadata implements Function<IMachine, NodeMetadata>
|
|||
INetworkAdapter adapter = vm.getNetworkAdapter(slot);
|
||||
if(adapter != null) {
|
||||
if (adapter.getAttachmentType() == NetworkAttachmentType.NAT) {
|
||||
publicIpAddresses.add(adapter.getNatDriver().getHostIP());
|
||||
String hostIP = adapter.getNatDriver().getHostIP();
|
||||
if(!hostIP.isEmpty())
|
||||
publicIpAddresses.add(hostIP);
|
||||
for (String nameProtocolnumberAddressInboudportGuestTargetport : adapter.getNatDriver().getRedirects()) {
|
||||
Iterable<String> stuff = Splitter.on(',').split(nameProtocolnumberAddressInboudportGuestTargetport);
|
||||
String protocolNumber = Iterables.get(stuff, 1);
|
||||
|
@ -153,26 +120,25 @@ public class IMachineToNodeMetadata implements Function<IMachine, NodeMetadata>
|
|||
String targetPort = Iterables.get(stuff, 5);
|
||||
if ("1".equals(protocolNumber) && "22".equals(targetPort)) {
|
||||
int inPort = Integer.parseInt(inboundPort);
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of(hostAddress)).loginPort(inPort);
|
||||
publicIpAddresses.add(hostAddress);
|
||||
nodeMetadataBuilder.loginPort(inPort);
|
||||
}
|
||||
//privateIpAddresses.add((NodeCreator.VMS_NETWORK + ipTermination) + "");
|
||||
}
|
||||
// TODO this could be a public and private address
|
||||
} else if (adapter.getAttachmentType() == NetworkAttachmentType.Bridged) {
|
||||
String clientIpAddress = machineUtils.getIpAddressFromBridgedNIC(vm.getName());
|
||||
String clientIpAddress = machineUtils.getIpAddressFromFirstNIC(vm.getName());
|
||||
//privateIpAddresses.add(clientIpAddress);
|
||||
publicIpAddresses.add(clientIpAddress);
|
||||
|
||||
} else if (adapter.getAttachmentType() == NetworkAttachmentType.HostOnly) {
|
||||
String clientIpAddress = machineUtils.getIpAddressFromHostOnlyNIC(vm.getName());
|
||||
String clientIpAddress = machineUtils.getIpAddressFromFirstNIC(vm.getName());
|
||||
publicIpAddresses.add(clientIpAddress);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
nodeMetadataBuilder.publicAddresses(publicIpAddresses);
|
||||
nodeMetadataBuilder.privateAddresses(publicIpAddresses);
|
||||
|
||||
return nodeMetadataBuilder;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ package org.jclouds.virtualbox.functions;
|
|||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_CREDENTIAL;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_IDENTITY;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -35,6 +37,7 @@ import org.jclouds.compute.reference.ComputeServiceConstants;
|
|||
import org.jclouds.domain.LoginCredentials;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.ssh.SshClient;
|
||||
import org.jclouds.virtualbox.VirtualBoxApiMetadata;
|
||||
import org.jclouds.virtualbox.domain.BridgedIf;
|
||||
import org.jclouds.virtualbox.statements.GetIPAddressFromMAC;
|
||||
import org.jclouds.virtualbox.statements.ScanNetworkWithPing;
|
||||
|
@ -82,10 +85,11 @@ public class IMachineToSshClient implements Function<IMachine, SshClient> {
|
|||
String clientIpAddress = null;
|
||||
String sshPort = "22";
|
||||
|
||||
// TODO: we need a way to align the default login credentials
|
||||
// from the iso with the vmspec -> IMachineToNodeMetadata using YamlImage ?
|
||||
String guestIdentity = VirtualBoxApiMetadata.defaultProperties().getProperty(VIRTUALBOX_GUEST_IDENTITY);
|
||||
String guestCredential = VirtualBoxApiMetadata.defaultProperties().getProperty(VIRTUALBOX_GUEST_CREDENTIAL);
|
||||
LoginCredentials loginCredentials = LoginCredentials.builder()
|
||||
.user("toor").password("password").authenticateSudo(true)
|
||||
.user(guestIdentity)
|
||||
.password(guestCredential).authenticateSudo(true)
|
||||
.build();
|
||||
|
||||
if (networkAdapter.getAttachmentType()
|
||||
|
@ -109,7 +113,7 @@ public class IMachineToSshClient implements Function<IMachine, SshClient> {
|
|||
clientIpAddress = getIpAddressFromBridgedNIC(networkAdapter, network);
|
||||
} else if (networkAdapter.getAttachmentType().equals(
|
||||
NetworkAttachmentType.HostOnly)) {
|
||||
clientIpAddress = machineUtils.getIpAddressFromHostOnlyNIC(vm.getName());
|
||||
clientIpAddress = machineUtils.getIpAddressFromFirstNIC(vm.getName());
|
||||
}
|
||||
|
||||
checkNotNull(clientIpAddress, "clientIpAddress");
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Licensed to jclouds, Inc. (jclouds) under one or more
|
||||
* contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. jclouds licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.jclouds.virtualbox.functions;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.virtualbox.util.MachineUtils;
|
||||
import org.virtualbox_4_1.VirtualBoxManager;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.cache.AbstractLoadingCache;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
/**
|
||||
* A {@link LoadingCache} for ip addresses. If the requested ip address has been
|
||||
* previously extracted this returns it, if not it calls vbox api.
|
||||
*
|
||||
* @author andrea turli
|
||||
*
|
||||
*/
|
||||
@Singleton
|
||||
public class IpAddressesLoadingCache extends
|
||||
AbstractLoadingCache<String, String> {
|
||||
|
||||
@Resource
|
||||
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
|
||||
protected Logger logger = Logger.NULL;
|
||||
|
||||
private final Map<String, String> masters = Maps.newHashMap();
|
||||
private final Supplier<VirtualBoxManager> manager;
|
||||
|
||||
@Inject
|
||||
public IpAddressesLoadingCache(Supplier<VirtualBoxManager> manager) {
|
||||
this.manager = checkNotNull(manager, "vboxmanager");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String get(String idOrName) throws ExecutionException {
|
||||
if (masters.containsKey(idOrName)) {
|
||||
return masters.get(idOrName);
|
||||
}
|
||||
|
||||
String currentIp = "", previousIp = "";
|
||||
int count = 0;
|
||||
while (count < 3) {
|
||||
currentIp = "";
|
||||
while (!MachineUtils.isIpv4(currentIp)) {
|
||||
currentIp = manager.get().getVBox().findMachine(idOrName)
|
||||
.getGuestPropertyValue("/VirtualBox/GuestInfo/Net/0/V4/IP");
|
||||
}
|
||||
|
||||
if (previousIp.equals(currentIp)) {
|
||||
count++;
|
||||
}
|
||||
previousIp = currentIp;
|
||||
}
|
||||
|
||||
masters.put(idOrName, currentIp);
|
||||
return currentIp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIfPresent(Object key) {
|
||||
return masters.get((String) key);
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,7 @@ package org.jclouds.virtualbox.functions;
|
|||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.jclouds.compute.options.RunScriptOptions.Builder.runAsRoot;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_DEFAULT_DIR;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_IMAGE_PREFIX;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_INSTALLATION_KEY_SEQUENCE;
|
||||
|
@ -32,6 +33,8 @@ import static org.jclouds.virtualbox.util.MachineUtils.machineNotFoundException;
|
|||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -42,10 +45,17 @@ import javax.inject.Inject;
|
|||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.jclouds.compute.callables.RunScriptOnNode.Factory;
|
||||
import org.jclouds.compute.domain.ExecResponse;
|
||||
import org.jclouds.compute.domain.Image;
|
||||
import org.jclouds.compute.domain.NodeMetadata;
|
||||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.location.Provider;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.rest.annotations.BuildVersion;
|
||||
import org.jclouds.scriptbuilder.domain.Statement;
|
||||
import org.jclouds.scriptbuilder.domain.StatementList;
|
||||
import org.jclouds.scriptbuilder.domain.Statements;
|
||||
import org.jclouds.virtualbox.domain.HardDisk;
|
||||
import org.jclouds.virtualbox.domain.IsoSpec;
|
||||
import org.jclouds.virtualbox.domain.Master;
|
||||
|
@ -57,6 +67,8 @@ import org.jclouds.virtualbox.domain.StorageController;
|
|||
import org.jclouds.virtualbox.domain.VmSpec;
|
||||
import org.jclouds.virtualbox.domain.YamlImage;
|
||||
import org.jclouds.virtualbox.functions.admin.PreseedCfgServer;
|
||||
import org.jclouds.virtualbox.predicates.RetryIfSocketNotYetOpen;
|
||||
import org.testng.collections.Lists;
|
||||
import org.virtualbox_4_1.CleanupMode;
|
||||
import org.virtualbox_4_1.IMachine;
|
||||
import org.virtualbox_4_1.NetworkAttachmentType;
|
||||
|
@ -71,178 +83,236 @@ import com.google.common.cache.AbstractLoadingCache;
|
|||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.net.HostAndPort;
|
||||
|
||||
/**
|
||||
* A {@link LoadingCache} for masters. If the requested master has been previously created this
|
||||
* returns it, if not it coordinates its creation including downloading isos and creating
|
||||
* cache/config directories. This also implements {@link Supplier} in order to provide jetty with
|
||||
* the current image (only one master can be created at a time).
|
||||
* A {@link LoadingCache} for masters. If the requested master has been
|
||||
* previously created this returns it, if not it coordinates its creation
|
||||
* including downloading isos and creating cache/config directories. This also
|
||||
* implements {@link Supplier} in order to provide jetty with the current image
|
||||
* (only one master can be created at a time).
|
||||
*
|
||||
* @author dralves
|
||||
* @author dralves, andrea turli
|
||||
*
|
||||
*/
|
||||
@Singleton
|
||||
public class MastersLoadingCache extends AbstractLoadingCache<Image, Master> {
|
||||
|
||||
// TODO parameterize
|
||||
public static final int MASTER_PORT = 2222;
|
||||
public static final String HOST_ONLY_IFACE_NAME = "vboxnet0";
|
||||
// TODO parameterize
|
||||
public static final int MASTER_PORT = 2222;
|
||||
|
||||
@Resource
|
||||
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
|
||||
protected Logger logger = Logger.NULL;
|
||||
@Resource
|
||||
@Named(ComputeServiceConstants.COMPUTE_LOGGER)
|
||||
protected Logger logger = Logger.NULL;
|
||||
|
||||
private final Map<String, Master> masters = Maps.newHashMap();
|
||||
private final Function<MasterSpec, IMachine> masterCreatorAndInstaller;
|
||||
private final Map<String, YamlImage> imageMapping;
|
||||
private final String workingDir;
|
||||
private final String installationKeySequence;
|
||||
private final String isosDir;
|
||||
private final Supplier<VirtualBoxManager> manager;
|
||||
private final Function<URI, File> isoDownloader;
|
||||
private final String version;
|
||||
private final String preconfigurationUrl;
|
||||
private final Map<String, Master> masters = Maps.newHashMap();
|
||||
private final Function<MasterSpec, IMachine> masterCreatorAndInstaller;
|
||||
private final Map<String, YamlImage> imageMapping;
|
||||
private final String workingDir;
|
||||
private final String installationKeySequence;
|
||||
private final String isosDir;
|
||||
private final Supplier<VirtualBoxManager> manager;
|
||||
private final String version;
|
||||
private final String preconfigurationUrl;
|
||||
|
||||
@Inject
|
||||
public MastersLoadingCache(@BuildVersion String version,
|
||||
@Named(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE) String installationKeySequence,
|
||||
@Named(VIRTUALBOX_PRECONFIGURATION_URL) String preconfigurationUrl,
|
||||
@Named(VIRTUALBOX_WORKINGDIR) String workingDir, Function<MasterSpec, IMachine> masterLoader,
|
||||
Supplier<Map<Image, YamlImage>> yamlMapper, Supplier<VirtualBoxManager> manager,
|
||||
Function<URI, File> isoDownloader) {
|
||||
checkNotNull(version, "version");
|
||||
checkNotNull(installationKeySequence, "installationKeySequence");
|
||||
checkNotNull(manager, "vboxmanager");
|
||||
this.manager = manager;
|
||||
this.masterCreatorAndInstaller = masterLoader;
|
||||
this.installationKeySequence = installationKeySequence;
|
||||
this.workingDir = workingDir == null ? VIRTUALBOX_DEFAULT_DIR : workingDir;
|
||||
this.isosDir = workingDir + File.separator + "isos";
|
||||
this.imageMapping = Maps.newLinkedHashMap();
|
||||
for (Entry<Image, YamlImage> entry : yamlMapper.get().entrySet()) {
|
||||
this.imageMapping.put(entry.getKey().getId(), entry.getValue());
|
||||
private final Factory runScriptOnNodeFactory;
|
||||
private final RetryIfSocketNotYetOpen socketTester;
|
||||
private final Supplier<NodeMetadata> host;
|
||||
private final Supplier<URI> providerSupplier;
|
||||
private final HardcodedHostToHostNodeMetadata hardcodedHostToHostNodeMetadata;
|
||||
|
||||
@Inject
|
||||
public MastersLoadingCache(
|
||||
@BuildVersion String version,
|
||||
@Named(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE) String installationKeySequence,
|
||||
@Named(VIRTUALBOX_PRECONFIGURATION_URL) String preconfigurationUrl,
|
||||
@Named(VIRTUALBOX_WORKINGDIR) String workingDir,
|
||||
Function<MasterSpec, IMachine> masterLoader,
|
||||
Supplier<Map<Image, YamlImage>> yamlMapper,
|
||||
Supplier<VirtualBoxManager> manager,
|
||||
Factory runScriptOnNodeFactory,
|
||||
RetryIfSocketNotYetOpen socketTester, Supplier<NodeMetadata> host,
|
||||
@Provider Supplier<URI> providerSupplier,
|
||||
HardcodedHostToHostNodeMetadata hardcodedHostToHostNodeMetadata) {
|
||||
checkNotNull(version, "version");
|
||||
checkNotNull(installationKeySequence, "installationKeySequence");
|
||||
checkNotNull(manager, "vboxmanager");
|
||||
this.manager = manager;
|
||||
this.masterCreatorAndInstaller = masterLoader;
|
||||
this.installationKeySequence = installationKeySequence;
|
||||
this.workingDir = workingDir == null ? VIRTUALBOX_DEFAULT_DIR
|
||||
: workingDir;
|
||||
this.isosDir = workingDir + File.separator + "isos";
|
||||
this.imageMapping = Maps.newLinkedHashMap();
|
||||
for (Entry<Image, YamlImage> entry : yamlMapper.get().entrySet()) {
|
||||
this.imageMapping.put(entry.getKey().getId(), entry.getValue());
|
||||
}
|
||||
this.version = Iterables.get(Splitter.on('r').split(version), 0);
|
||||
this.preconfigurationUrl = preconfigurationUrl;
|
||||
|
||||
this.runScriptOnNodeFactory = checkNotNull(runScriptOnNodeFactory,
|
||||
"runScriptOnNodeFactory");
|
||||
this.socketTester = checkNotNull(socketTester, "socketTester");
|
||||
this.socketTester.seconds(3L);
|
||||
this.host = checkNotNull(host, "host");
|
||||
this.providerSupplier = checkNotNull(providerSupplier,
|
||||
"endpoint to virtualbox websrvd is needed");
|
||||
this.hardcodedHostToHostNodeMetadata = hardcodedHostToHostNodeMetadata;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void createCacheDirStructure() {
|
||||
if (!new File(workingDir).exists()) {
|
||||
new File(workingDir, "isos").mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Master get(Image key) throws ExecutionException {
|
||||
// check if we have loaded this machine before
|
||||
if (masters.containsKey(key.getId())) {
|
||||
return masters.get(key.getId());
|
||||
}
|
||||
checkState(!key.getId().contains(VIRTUALBOX_NODE_NAME_SEPARATOR),
|
||||
"master image names cannot contain \""
|
||||
+ VIRTUALBOX_NODE_NAME_SEPARATOR + "\"");
|
||||
String vmName = VIRTUALBOX_IMAGE_PREFIX + key.getId();
|
||||
IMachine masterMachine;
|
||||
Master master;
|
||||
// ready the preseed file server
|
||||
PreseedCfgServer server = new PreseedCfgServer();
|
||||
try {
|
||||
// try and find a master machine in vbox
|
||||
masterMachine = manager.get().getVBox().findMachine(vmName);
|
||||
master = Master.builder().machine(masterMachine).build();
|
||||
} catch (VBoxException e) {
|
||||
if (machineNotFoundException(e)) {
|
||||
// machine was not found try to build one from a yaml file
|
||||
YamlImage currentImage = checkNotNull(imageMapping.get(key.getId()), "currentImage");
|
||||
URI preseedServer;
|
||||
try {
|
||||
preseedServer = new URI(preconfigurationUrl);
|
||||
if (!socketTester.apply(HostAndPort.fromParts(preseedServer.getHost(),
|
||||
preseedServer.getPort()))) {
|
||||
server.start(preconfigurationUrl, currentImage.preseed_cfg);
|
||||
}
|
||||
} catch (URISyntaxException e1) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
MasterSpec masterSpec = buildMasterSpecFromYaml(currentImage,
|
||||
vmName);
|
||||
|
||||
// create the master machine if it can't be found
|
||||
masterMachine = masterCreatorAndInstaller.apply(masterSpec);
|
||||
// build the master
|
||||
master = Master.builder().machine(masterMachine)
|
||||
.spec(masterSpec).build();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
masters.put(key.getId(), master);
|
||||
return master;
|
||||
}
|
||||
|
||||
private MasterSpec buildMasterSpecFromYaml(YamlImage currentImage,
|
||||
String vmName) throws ExecutionException {
|
||||
String guestAdditionsFileName = String.format(
|
||||
"VBoxGuestAdditions_%s.iso", version);
|
||||
String guestAdditionsIso = String.format("%s/%s", isosDir,
|
||||
guestAdditionsFileName);
|
||||
String guestAdditionsUri = "http://download.virtualbox.org/virtualbox/"
|
||||
+ version + "/" + guestAdditionsFileName;
|
||||
if (!new File(guestAdditionsIso).exists()) {
|
||||
getFilePathOrDownload(guestAdditionsUri, null);
|
||||
}
|
||||
// check if the iso is here, download if not
|
||||
String localIsoUrl = checkNotNull(getFilePathOrDownload(currentImage.iso, currentImage.iso_md5), "distro iso");
|
||||
String adminDisk = workingDir + File.separator + vmName + ".vdi";
|
||||
HardDisk hardDisk = HardDisk.builder().diskpath(adminDisk)
|
||||
.autoDelete(true).controllerPort(0).deviceSlot(1).build();
|
||||
|
||||
StorageController ideController = StorageController.builder()
|
||||
.name("IDE Controller").bus(StorageBus.IDE)
|
||||
.attachISO(0, 0, localIsoUrl).attachHardDisk(hardDisk)
|
||||
.attachISO(1, 0, guestAdditionsIso).build();
|
||||
|
||||
VmSpec vmSpecification = VmSpec.builder().id(currentImage.id)
|
||||
.name(vmName).memoryMB(512).osTypeId("")
|
||||
.controller(ideController).forceOverwrite(true)
|
||||
.cleanUpMode(CleanupMode.Full).build();
|
||||
|
||||
NetworkAdapter networkAdapter = NetworkAdapter
|
||||
.builder()
|
||||
.networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.tcpRedirectRule(providerSupplier.get().getHost(), MASTER_PORT,
|
||||
"", 22).build();
|
||||
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard
|
||||
.builder().addNetworkAdapter(networkAdapter).slot(0L).build();
|
||||
|
||||
NetworkSpec networkSpec = NetworkSpec.builder()
|
||||
.addNIC(networkInterfaceCard).build();
|
||||
|
||||
return MasterSpec
|
||||
.builder()
|
||||
.vm(vmSpecification)
|
||||
.iso(IsoSpec
|
||||
.builder()
|
||||
.sourcePath(localIsoUrl)
|
||||
.installationScript(
|
||||
installationKeySequence.replace("HOSTNAME",
|
||||
vmSpecification.getVmName())).build())
|
||||
.network(networkSpec).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Master getIfPresent(Object key) {
|
||||
checkArgument(key instanceof Image,
|
||||
"this cache is for entries who's keys are Images");
|
||||
Image image = Image.class.cast(key);
|
||||
if (masters.containsKey(image.getId())) {
|
||||
return masters.get(image.getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getFilePathOrDownload(String httpUrl, String md5)
|
||||
throws ExecutionException {
|
||||
String fileName = httpUrl.substring(httpUrl.lastIndexOf('/') + 1,
|
||||
httpUrl.length());
|
||||
URI provider = providerSupplier.get();
|
||||
if (!socketTester.apply(HostAndPort.fromParts(provider.getHost(),
|
||||
provider.getPort()))) {
|
||||
throw new RuntimeException("could not connect to virtualbox");
|
||||
}
|
||||
this.version = Iterables.get(Splitter.on('r').split(version), 0);
|
||||
this.isoDownloader = isoDownloader;
|
||||
this.preconfigurationUrl = preconfigurationUrl;
|
||||
}
|
||||
File file = new File(isosDir, fileName);
|
||||
List<Statement> statements = Lists.newArrayList();
|
||||
statements.add(Statements.saveHttpResponseTo(URI.create(httpUrl),
|
||||
isosDir, fileName));
|
||||
StatementList statementList = new StatementList(statements);
|
||||
NodeMetadata hostNodeMetadata = hardcodedHostToHostNodeMetadata
|
||||
.apply(host.get());
|
||||
runScriptOnNodeFactory
|
||||
.create(hostNodeMetadata, statementList, runAsRoot(false)).init()
|
||||
.call();
|
||||
|
||||
@PostConstruct
|
||||
public void createCacheDirStructure() {
|
||||
if (!new File(workingDir).exists()) {
|
||||
new File(workingDir, "isos").mkdirs();
|
||||
ExecResponse response = runScriptOnNodeFactory
|
||||
.create(
|
||||
hostNodeMetadata,
|
||||
Statements.exec("md5 " + isosDir + File.separator + fileName),
|
||||
runAsRoot(false)).init().call();
|
||||
if (md5 != null) {
|
||||
if (!Iterables.get(
|
||||
Splitter.on("=").trimResults().split(response.getOutput()), 1)
|
||||
.equals(md5))
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Master get(Image key) throws ExecutionException {
|
||||
// check if we have loaded this machine before
|
||||
if (masters.containsKey(key.getId())) {
|
||||
return masters.get(key.getId());
|
||||
}
|
||||
|
||||
checkState(!key.getId().contains(VIRTUALBOX_NODE_NAME_SEPARATOR), "master image names cannot contain \""
|
||||
+ VIRTUALBOX_NODE_NAME_SEPARATOR + "\"");
|
||||
|
||||
String vmName = VIRTUALBOX_IMAGE_PREFIX + key.getId();
|
||||
|
||||
IMachine masterMachine;
|
||||
|
||||
Master master;
|
||||
|
||||
// ready the preseed file server
|
||||
PreseedCfgServer server = new PreseedCfgServer();
|
||||
try {
|
||||
// try and find a master machine in vbox
|
||||
masterMachine = manager.get().getVBox().findMachine(vmName);
|
||||
master = Master.builder().machine(masterMachine).build();
|
||||
|
||||
} catch (VBoxException e) {
|
||||
if (machineNotFoundException(e)) {
|
||||
// machine was not found try to build one from a yaml file
|
||||
YamlImage currentImage = imageMapping.get(key.getId());
|
||||
|
||||
checkNotNull(currentImage);
|
||||
|
||||
server.start(preconfigurationUrl, currentImage.preseed_cfg);
|
||||
|
||||
MasterSpec masterSpec = buildMasterSpecFromYaml(currentImage, vmName);
|
||||
|
||||
// create the master machine if it can't be found
|
||||
masterMachine = masterCreatorAndInstaller.apply(masterSpec);
|
||||
|
||||
// build the master
|
||||
master = Master.builder().machine(masterMachine).spec(masterSpec).build();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
masters.put(key.getId(), master);
|
||||
return master;
|
||||
}
|
||||
|
||||
private MasterSpec buildMasterSpecFromYaml(YamlImage currentImage, String vmName) throws ExecutionException {
|
||||
|
||||
String guestAdditionsFileName = String.format("VBoxGuestAdditions_%s.iso", version);
|
||||
String guestAdditionsIso = String.format("%s/%s", isosDir, guestAdditionsFileName);
|
||||
String guestAdditionsUri = "http://download.virtualbox.org/virtualbox/" + version + "/" + guestAdditionsFileName;
|
||||
if (!new File(guestAdditionsIso).exists()) {
|
||||
getFilePathOrDownload(guestAdditionsUri);
|
||||
}
|
||||
checkState(new File(guestAdditionsIso).exists(), "guest additions iso does not exist at: " + guestAdditionsIso);
|
||||
|
||||
// check if the iso is here, download if not
|
||||
String localIsoUrl = getFilePathOrDownload(currentImage.iso);
|
||||
|
||||
String adminDisk = workingDir + File.separator + vmName + ".vdi";
|
||||
|
||||
HardDisk hardDisk = HardDisk.builder().diskpath(adminDisk).autoDelete(true).controllerPort(0).deviceSlot(1)
|
||||
.build();
|
||||
|
||||
StorageController ideController = StorageController.builder().name("IDE Controller").bus(StorageBus.IDE)
|
||||
.attachISO(0, 0, localIsoUrl).attachHardDisk(hardDisk).attachISO(1, 0, guestAdditionsIso).build();
|
||||
|
||||
VmSpec vmSpecification = VmSpec.builder().id(currentImage.id).name(vmName).memoryMB(512).osTypeId("")
|
||||
.controller(ideController).forceOverwrite(true).cleanUpMode(CleanupMode.Full).build();
|
||||
|
||||
NetworkAdapter networkAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.tcpRedirectRule("127.0.0.1", MASTER_PORT, "", 22).build();
|
||||
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(networkAdapter)
|
||||
.slot(0L).build();
|
||||
|
||||
NetworkSpec networkSpec = NetworkSpec.builder().addNIC(networkInterfaceCard).build();
|
||||
|
||||
return MasterSpec
|
||||
.builder()
|
||||
.vm(vmSpecification)
|
||||
.iso(IsoSpec.builder().sourcePath(localIsoUrl)
|
||||
.installationScript(installationKeySequence.replace("HOSTNAME", vmSpecification.getVmName()))
|
||||
.build()).network(networkSpec).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Master getIfPresent(Object key) {
|
||||
checkArgument(key instanceof Image, "this cache is for entries who's keys are Images");
|
||||
Image image = Image.class.cast(key);
|
||||
if (masters.containsKey(image.getId())) {
|
||||
return masters.get(image.getId());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getFilePathOrDownload(String httpUrl) throws ExecutionException {
|
||||
String fileName = httpUrl.substring(httpUrl.lastIndexOf('/') + 1, httpUrl.length());
|
||||
File localFile = new File(isosDir, fileName);
|
||||
if (!localFile.exists()) {
|
||||
logger.debug("iso not found in cache, downloading: %s", httpUrl);
|
||||
localFile = isoDownloader.apply(URI.create(httpUrl));
|
||||
}
|
||||
checkState(localFile.exists(), "iso file has not been downloaded: " + fileName);
|
||||
return localFile.getAbsolutePath();
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,10 +22,13 @@ package org.jclouds.virtualbox.functions;
|
|||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static org.jclouds.compute.options.RunScriptOptions.Builder.runAsRoot;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_CREDENTIAL;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_GUEST_IDENTITY;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_IMAGE_PREFIX;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_NODE_NAME_SEPARATOR;
|
||||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_NODE_PREFIX;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
@ -39,7 +42,12 @@ import org.jclouds.compute.domain.NodeMetadata;
|
|||
import org.jclouds.compute.domain.NodeMetadataBuilder;
|
||||
import org.jclouds.compute.options.RunScriptOptions;
|
||||
import org.jclouds.domain.LoginCredentials;
|
||||
import org.jclouds.javax.annotation.Nullable;
|
||||
import org.jclouds.location.Provider;
|
||||
import org.jclouds.rest.annotations.Credential;
|
||||
import org.jclouds.rest.annotations.Identity;
|
||||
import org.jclouds.scriptbuilder.domain.Statements;
|
||||
import org.jclouds.virtualbox.VirtualBoxApiMetadata;
|
||||
import org.jclouds.virtualbox.config.VirtualBoxComputeServiceContextModule;
|
||||
import org.jclouds.virtualbox.domain.CloneSpec;
|
||||
import org.jclouds.virtualbox.domain.Master;
|
||||
|
@ -63,8 +71,10 @@ import org.virtualbox_4_1.NetworkAttachmentType;
|
|||
import org.virtualbox_4_1.VirtualBoxManager;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
|
@ -84,29 +94,36 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
private final MachineController machineController;
|
||||
private final Factory runScriptOnNodeFactory;
|
||||
private final Supplier<NodeMetadata> host;
|
||||
|
||||
private final Supplier<URI> providerSupplier;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private int ram = 512;
|
||||
private final String guestIdentity = VirtualBoxApiMetadata.defaultProperties().getProperty(VIRTUALBOX_GUEST_IDENTITY);
|
||||
private final String guestCredential = VirtualBoxApiMetadata.defaultProperties().getProperty(VIRTUALBOX_GUEST_CREDENTIAL);
|
||||
|
||||
@Inject
|
||||
public NodeCreator(Supplier<VirtualBoxManager> manager, Function<CloneSpec, IMachine> cloner, Factory runScriptOnNodeFactory,
|
||||
MachineUtils machineUtils, RunScriptOnNode.Factory scriptRunnerFactory, MachineController machineController,
|
||||
Supplier<NodeMetadata> host,
|
||||
MachineUtils machineUtils, RunScriptOnNode.Factory scriptRunnerFactory, MachineController machineController) {
|
||||
@Provider Supplier<URI> providerSupplier,
|
||||
@Nullable @Identity String identity,
|
||||
@Nullable @Credential String credential) {
|
||||
this.manager = manager;
|
||||
this.cloner = cloner;
|
||||
this.runScriptOnNodeFactory = checkNotNull(runScriptOnNodeFactory, "runScriptOnNodeFactory");
|
||||
this.host = checkNotNull(host, "host");
|
||||
|
||||
this.machineUtils = machineUtils;
|
||||
this.machineController = machineController;
|
||||
this.host = checkNotNull(host, "host");
|
||||
this.providerSupplier = checkNotNull(providerSupplier,
|
||||
"endpoint to virtualbox websrvd is needed");
|
||||
this.username = identity;
|
||||
this.password = credential;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized NodeAndInitialCredentials<IMachine> apply(NodeSpec nodeSpec) {
|
||||
|
||||
checkNotNull(nodeSpec, "NodeSpec");
|
||||
|
||||
Master master = nodeSpec.getMaster();
|
||||
checkNotNull(master, "Master");
|
||||
|
||||
Master master = checkNotNull(nodeSpec.getMaster(), "Master");
|
||||
if (master.getMachine().getCurrentSnapshot() != null) {
|
||||
ISession session;
|
||||
try {
|
||||
|
@ -119,40 +136,32 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
session.unlockMachine();
|
||||
}
|
||||
String masterNameWithoutPrefix = master.getMachine().getName().replace(VIRTUALBOX_IMAGE_PREFIX, "");
|
||||
|
||||
String cloneName = VIRTUALBOX_NODE_PREFIX + masterNameWithoutPrefix + VIRTUALBOX_NODE_NAME_SEPARATOR
|
||||
+ nodeSpec.getTag() + VIRTUALBOX_NODE_NAME_SEPARATOR + nodeSpec.getName();
|
||||
|
||||
int ram = 512;
|
||||
if (nodeSpec.getTemplate() != null && nodeSpec.getTemplate().getHardware() != null
|
||||
&& nodeSpec.getTemplate().getHardware().getRam() > 0) {
|
||||
ram = nodeSpec.getTemplate().getHardware().getRam();
|
||||
}
|
||||
|
||||
VmSpec cloneVmSpec = VmSpec.builder().id(cloneName).name(cloneName).memoryMB(ram).cleanUpMode(CleanupMode.Full)
|
||||
VmSpec cloneVmSpec = VmSpec.builder().id(cloneName).name(cloneName).memoryMB(ram)
|
||||
.cleanUpMode(CleanupMode.Full)
|
||||
.forceOverwrite(true).build();
|
||||
|
||||
// CASE NAT + HOST-ONLY
|
||||
NetworkAdapter natAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.build();
|
||||
NetworkInterfaceCard natIfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(natAdapter).slot(1L).build();
|
||||
|
||||
NetworkAdapter hostOnlyAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.HostOnly)
|
||||
.build();
|
||||
|
||||
// create new hostOnly interface if needed, otherwise use the one already there with dhcp enabled ...
|
||||
String hostOnlyIfName = getHostOnlyIfOrCreate();
|
||||
|
||||
NetworkInterfaceCard hostOnlyIfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(hostOnlyAdapter)
|
||||
.addHostInterfaceName(hostOnlyIfName).slot(0L).build();
|
||||
|
||||
NetworkSpec networkSpec = createNetworkSpecForHostOnlyNATNICs(natIfaceCard, hostOnlyIfaceCard);
|
||||
// case 'vbox host is localhost': NAT + HOST-ONLY
|
||||
NetworkSpec networkSpec = createNetworkSpecWhenVboxIsLocalhost();
|
||||
Optional<NetworkInterfaceCard> optionalNatIfaceCard = Iterables.tryFind(
|
||||
networkSpec.getNetworkInterfaceCards(),
|
||||
new Predicate<NetworkInterfaceCard>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(NetworkInterfaceCard nic) {
|
||||
return nic.getNetworkAdapter().getNetworkAttachmentType()
|
||||
.equals(NetworkAttachmentType.NAT);
|
||||
}
|
||||
});
|
||||
CloneSpec cloneSpec = CloneSpec.builder().linked(true).master(master.getMachine()).network(networkSpec)
|
||||
.vm(cloneVmSpec).build();
|
||||
|
||||
IMachine cloned = cloner.apply(cloneSpec);
|
||||
|
||||
machineController.ensureMachineIsLaunched(cloneVmSpec.getVmName());
|
||||
|
||||
// IMachineToNodeMetadata produces the final ip's but these need to be set before so we build a
|
||||
|
@ -160,42 +169,55 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
NodeMetadata partialNodeMetadata = buildPartialNodeMetadata(cloned);
|
||||
|
||||
// see DeleteGShadowLock for a detailed explanation
|
||||
machineUtils.runScriptOnNode(partialNodeMetadata, new DeleteGShadowLock(), RunScriptOptions.NONE);
|
||||
machineUtils.runScriptOnNode(partialNodeMetadata, new DeleteGShadowLock(), RunScriptOptions.NONE);
|
||||
|
||||
// CASE NAT + HOST-ONLY
|
||||
machineUtils.runScriptOnNode(partialNodeMetadata, new EnableNetworkInterface(natIfaceCard), RunScriptOptions.NONE);
|
||||
if(optionalNatIfaceCard.isPresent())
|
||||
machineUtils.runScriptOnNode(partialNodeMetadata, new EnableNetworkInterface(optionalNatIfaceCard.get()), RunScriptOptions.NONE);
|
||||
|
||||
|
||||
// TODO get credentials from somewhere else (they are also HC in
|
||||
// IMachineToSshClient)
|
||||
NodeAndInitialCredentials<IMachine> nodeAndInitialCredentials = new NodeAndInitialCredentials<IMachine>(cloned,
|
||||
cloneName, LoginCredentials.builder().user("toor").password("password").authenticateSudo(true).build());
|
||||
|
||||
return nodeAndInitialCredentials;
|
||||
return new NodeAndInitialCredentials<IMachine>(cloned,
|
||||
cloneName, LoginCredentials.builder()
|
||||
.user(guestIdentity)
|
||||
.password(guestCredential)
|
||||
.authenticateSudo(true)
|
||||
.build());
|
||||
}
|
||||
|
||||
private NodeMetadata buildPartialNodeMetadata(IMachine clone) {
|
||||
NodeMetadataBuilder nodeMetadataBuilder = new NodeMetadataBuilder();
|
||||
nodeMetadataBuilder.id(clone.getName());
|
||||
nodeMetadataBuilder.status(VirtualBoxComputeServiceContextModule.toPortableNodeStatus.get(clone.getState()));
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of(machineUtils.getIpAddressFromHostOnlyNIC(clone.getName())));
|
||||
|
||||
LoginCredentials loginCredentials = new LoginCredentials("toor", "password", null, true);
|
||||
nodeMetadataBuilder.publicAddresses(ImmutableSet.of(machineUtils.getIpAddressFromFirstNIC(clone.getName())));
|
||||
LoginCredentials loginCredentials = new LoginCredentials(guestIdentity, guestCredential, null, true);
|
||||
nodeMetadataBuilder.credentials(loginCredentials);
|
||||
|
||||
return nodeMetadataBuilder.build();
|
||||
}
|
||||
|
||||
private NetworkSpec createNetworkSpecWhenVboxIsLocalhost() {
|
||||
NetworkAdapter natAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.build();
|
||||
NetworkInterfaceCard natIfaceCard = NetworkInterfaceCard.builder()
|
||||
.addNetworkAdapter(natAdapter)
|
||||
.slot(1L)
|
||||
.build();
|
||||
NetworkAdapter hostOnlyAdapter = NetworkAdapter.builder()
|
||||
.networkAttachmentType(NetworkAttachmentType.HostOnly)
|
||||
.build();
|
||||
// create new hostOnly interface if needed, otherwise use the one already there with dhcp enabled ...
|
||||
String hostOnlyIfName = getHostOnlyIfOrCreate();
|
||||
NetworkInterfaceCard hostOnlyIfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(hostOnlyAdapter)
|
||||
.addHostInterfaceName(hostOnlyIfName).slot(0L).build();
|
||||
return createNetworkSpecForHostOnlyNATNICs(natIfaceCard, hostOnlyIfaceCard);
|
||||
}
|
||||
|
||||
private NetworkSpec createNetworkSpecForHostOnlyNATNICs(NetworkInterfaceCard natIfaceCard,
|
||||
NetworkInterfaceCard hostOnlyIfaceCard) {
|
||||
return NetworkSpec.builder().addNIC(natIfaceCard).addNIC(hostOnlyIfaceCard).build();
|
||||
return NetworkSpec.builder()
|
||||
.addNIC(natIfaceCard)
|
||||
.addNIC(hostOnlyIfaceCard)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
private String getHostOnlyIfOrCreate() {
|
||||
|
||||
IHostNetworkInterface availableHostInterfaceIf = returnExistingHostNetworkInterfaceWithDHCPenabledOrNull(manager
|
||||
.get().getVBox().getHost().getNetworkInterfaces());
|
||||
if (availableHostInterfaceIf==null) {
|
||||
|
@ -205,7 +227,6 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
} else {
|
||||
return availableHostInterfaceIf.getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void assignDHCPtoHostOnlyInterface(final String hostOnlyIfName) {
|
||||
|
@ -225,25 +246,21 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
String dhcpNetmask = "255.255.255.0";
|
||||
String dhcpLowerIp = hostOnlyIfIpAddress.substring(0, hostOnlyIfIpAddress.lastIndexOf(".")) + ".2";
|
||||
String dhcpUpperIp = hostOnlyIfIpAddress.substring(0, hostOnlyIfIpAddress.lastIndexOf(".")) + ".253";
|
||||
NodeMetadata hostNodeMetadata = getHostNodeMetadata();
|
||||
|
||||
ExecResponse response = runScriptOnNodeFactory
|
||||
.create(host.get(),
|
||||
.create(hostNodeMetadata,
|
||||
Statements.exec(String
|
||||
.format("VBoxManage dhcpserver add --ifname %s --ip %s --netmask %s --lowerip %s --upperip %s --enable",
|
||||
hostOnlyIfName, dhcpIpAddress, dhcpNetmask, dhcpLowerIp, dhcpUpperIp)), runAsRoot(false).wrapInInitScript(false)).init().call();
|
||||
checkState(response.getExitStatus()==0);
|
||||
/*
|
||||
runScriptOnNodeFactory
|
||||
.create(host.get(),
|
||||
Statements.exec(String.format("VBoxManage hostonlyif ipconfig %s --ip %s",
|
||||
hostOnlyIfName, hostOnlyIfIpAddress)), runAsRoot(false).wrapInInitScript(false)).init().call();
|
||||
*/
|
||||
}
|
||||
|
||||
private String createHostOnlyIf() {
|
||||
final String hostOnlyIfName;
|
||||
NodeMetadata hostNodeMetadata = getHostNodeMetadata();
|
||||
ExecResponse createHostOnlyResponse = runScriptOnNodeFactory
|
||||
.create(host.get(), Statements.exec("VBoxManage hostonlyif create"),
|
||||
.create(hostNodeMetadata, Statements.exec("VBoxManage hostonlyif create"),
|
||||
runAsRoot(false).wrapInInitScript(false)).init().call();
|
||||
String output = createHostOnlyResponse.getOutput();
|
||||
checkState(createHostOnlyResponse.getExitStatus()==0);
|
||||
|
@ -252,6 +269,16 @@ public class NodeCreator implements Function<NodeSpec, NodeAndInitialCredentials
|
|||
return hostOnlyIfName;
|
||||
}
|
||||
|
||||
private NodeMetadata getHostNodeMetadata() {
|
||||
NodeMetadata hostNodeMetadata = NodeMetadataBuilder
|
||||
.fromNodeMetadata(host.get())
|
||||
.credentials(LoginCredentials.builder().user(username).password(password).build())
|
||||
.publicAddresses(
|
||||
ImmutableList.of(providerSupplier.get().getHost()))
|
||||
.build();
|
||||
return hostNodeMetadata;
|
||||
}
|
||||
|
||||
private IHostNetworkInterface returnExistingHostNetworkInterfaceWithDHCPenabledOrNull(Iterable<IHostNetworkInterface> availableNetworkInterfaces) {
|
||||
checkNotNull(availableNetworkInterfaces);
|
||||
return Iterables.getFirst(filterAvailableNetworkInterfaceByHostOnlyAndDHCPenabled(availableNetworkInterfaces), null);
|
||||
|
|
|
@ -24,22 +24,29 @@ import static com.google.common.base.Preconditions.checkState;
|
|||
import static org.jclouds.compute.options.RunScriptOptions.Builder.runAsRoot;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.jclouds.compute.callables.RunScriptOnNode.Factory;
|
||||
import org.jclouds.compute.domain.ExecResponse;
|
||||
import org.jclouds.compute.domain.NodeMetadata;
|
||||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.location.Provider;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.scriptbuilder.domain.Statement;
|
||||
import org.jclouds.scriptbuilder.domain.StatementList;
|
||||
import org.jclouds.scriptbuilder.domain.Statements;
|
||||
import org.jclouds.virtualbox.functions.HardcodedHostToHostNodeMetadata;
|
||||
import org.jclouds.virtualbox.predicates.RetryIfSocketNotYetOpen;
|
||||
import org.virtualbox_4_1.SessionState;
|
||||
import org.virtualbox_4_1.VirtualBoxManager;
|
||||
|
||||
import com.beust.jcommander.internal.Lists;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.net.HostAndPort;
|
||||
|
@ -57,41 +64,72 @@ public class StartVBoxIfNotAlreadyRunning implements Supplier<VirtualBoxManager>
|
|||
private final Supplier<URI> providerSupplier;
|
||||
private final Function<Supplier<NodeMetadata>, VirtualBoxManager> managerForNode;
|
||||
private transient VirtualBoxManager manager;
|
||||
private final HardcodedHostToHostNodeMetadata hardcodedHostToHostNodeMetadata;
|
||||
|
||||
// the functions and suppliers here are to ensure we don't do heavy i/o in injection
|
||||
@Inject
|
||||
public StartVBoxIfNotAlreadyRunning(Function<Supplier<NodeMetadata>, VirtualBoxManager> managerForNode,
|
||||
Factory runScriptOnNodeFactory, RetryIfSocketNotYetOpen socketTester, Supplier<NodeMetadata> host,
|
||||
@Provider Supplier<URI> providerSupplier) {
|
||||
@Provider Supplier<URI> providerSupplier,
|
||||
HardcodedHostToHostNodeMetadata hardcodedHostToHostNodeMetadata) {
|
||||
this.runScriptOnNodeFactory = checkNotNull(runScriptOnNodeFactory, "runScriptOnNodeFactory");
|
||||
this.socketTester = checkNotNull(socketTester, "socketTester");
|
||||
this.socketTester.seconds(3L);
|
||||
this.host = checkNotNull(host, "host");
|
||||
this.providerSupplier = checkNotNull(providerSupplier, "endpoint to virtualbox websrvd is needed");
|
||||
this.managerForNode = checkNotNull(managerForNode, "managerForNode");
|
||||
start();
|
||||
this.hardcodedHostToHostNodeMetadata = hardcodedHostToHostNodeMetadata;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public synchronized void start() {
|
||||
URI provider = providerSupplier.get();
|
||||
if (!socketTester.apply(HostAndPort.fromParts(provider.getHost(), provider.getPort()))) {
|
||||
logger.debug("disabling password access");
|
||||
runScriptOnNodeFactory.create(host.get(), Statements.exec("VBoxManage setproperty websrvauthlibrary null"),
|
||||
runAsRoot(false).wrapInInitScript(false)).init().call();
|
||||
logger.debug(">> starting vboxwebsrv");
|
||||
String vboxwebsrv = "vboxwebsrv -t 10000 -v -b";
|
||||
runScriptOnNodeFactory.create(host.get(), Statements.exec(vboxwebsrv),
|
||||
runAsRoot(false).wrapInInitScript(false).blockOnComplete(false).nameTask("vboxwebsrv")).init().call();
|
||||
NodeMetadata hostNodeMetadata = hardcodedHostToHostNodeMetadata.apply(host.get());
|
||||
// kill previously started vboxwebsrv (possibly dirty session)
|
||||
List<Statement> statements = Lists.newArrayList();
|
||||
statements.add(Statements.findPid("vboxwebsrv"));
|
||||
statements.add(Statements.kill());
|
||||
StatementList statementList = new StatementList(statements);
|
||||
|
||||
if (!socketTester.apply(HostAndPort.fromParts(provider.getHost(), provider.getPort()))){
|
||||
throw new RuntimeException("could not connect to virtualbox");
|
||||
}
|
||||
if (socketTester.apply(HostAndPort.fromParts(provider.getHost(),
|
||||
provider.getPort()))) {
|
||||
logger.debug(String.format("shutting down previously started vboxwewbsrv at %s", provider));
|
||||
ExecResponse execResponse = runScriptOnNodeFactory
|
||||
.create(hostNodeMetadata, statementList, runAsRoot(false))
|
||||
.init().call();
|
||||
if(execResponse.getExitStatus()!=0)
|
||||
throw new RuntimeException("Cannot execute jclouds");
|
||||
}
|
||||
|
||||
logger.debug("disabling password access");
|
||||
runScriptOnNodeFactory
|
||||
.create(
|
||||
hostNodeMetadata,
|
||||
Statements
|
||||
.exec("VBoxManage setproperty websrvauthlibrary null"),
|
||||
runAsRoot(false).wrapInInitScript(false)).init().call();
|
||||
logger.debug(">> starting vboxwebsrv");
|
||||
String vboxwebsrv = "vboxwebsrv -t0 -v -b -H "
|
||||
+ providerSupplier.get().getHost();
|
||||
runScriptOnNodeFactory
|
||||
.create(
|
||||
hostNodeMetadata,
|
||||
Statements.exec(vboxwebsrv),
|
||||
runAsRoot(false).wrapInInitScript(false)
|
||||
.blockOnComplete(false).nameTask("vboxwebsrv")).init()
|
||||
.call();
|
||||
|
||||
if (!socketTester.apply(HostAndPort.fromParts(provider.getHost(),
|
||||
provider.getPort()))) {
|
||||
throw new RuntimeException("could not connect to virtualbox");
|
||||
}
|
||||
|
||||
manager = managerForNode.apply(host);
|
||||
manager.connect(provider.toASCIIString(), "", "");
|
||||
if (logger.isDebugEnabled())
|
||||
if (manager.getSessionObject().getState() != SessionState.Unlocked)
|
||||
logger.warn("manager is not in unlocked state " + manager.getSessionObject().getState());
|
||||
logger.warn("manager is not in unlocked state "
|
||||
+ manager.getSessionObject().getState());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -63,9 +63,6 @@ public class EnableNetworkInterface implements Statement {
|
|||
}
|
||||
|
||||
private List<Statement> getStatements(String iface) {
|
||||
/*
|
||||
* auto eth0
|
||||
*/
|
||||
List<Statement> statements = Lists.newArrayList();
|
||||
statements.add(exec(String.format("echo auto %s >> /etc/network/interfaces", iface))); //
|
||||
statements.add(exec(String.format("echo iface %s inet dhcp >> /etc/network/interfaces", iface))); //
|
||||
|
|
|
@ -21,6 +21,7 @@ package org.jclouds.virtualbox.util;
|
|||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -37,6 +38,7 @@ import org.jclouds.compute.reference.ComputeServiceConstants;
|
|||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.scriptbuilder.domain.Statement;
|
||||
import org.jclouds.util.Throwables2;
|
||||
import org.jclouds.virtualbox.functions.IpAddressesLoadingCache;
|
||||
import org.virtualbox_4_1.IMachine;
|
||||
import org.virtualbox_4_1.ISession;
|
||||
import org.virtualbox_4_1.LockType;
|
||||
|
@ -69,12 +71,14 @@ public class MachineUtils {
|
|||
|
||||
private final Supplier<VirtualBoxManager> manager;
|
||||
private final Factory scriptRunner;
|
||||
private final IpAddressesLoadingCache ipAddressesLoadingCache;
|
||||
|
||||
@Inject
|
||||
public MachineUtils(Supplier<VirtualBoxManager> manager, RunScriptOnNode.Factory scriptRunner) {
|
||||
super();
|
||||
public MachineUtils(Supplier<VirtualBoxManager> manager, RunScriptOnNode.Factory scriptRunner,
|
||||
IpAddressesLoadingCache ipAddressesLoadingCache) {
|
||||
this.manager = manager;
|
||||
this.scriptRunner = scriptRunner;
|
||||
this.ipAddressesLoadingCache = ipAddressesLoadingCache;
|
||||
}
|
||||
|
||||
public ListenableFuture<ExecResponse> runScriptOnNode(NodeMetadata metadata, Statement statement,
|
||||
|
@ -207,9 +211,18 @@ public class MachineUtils {
|
|||
private ISession lockSession(String machineId, LockType type, int retries) {
|
||||
int count = 0;
|
||||
ISession session;
|
||||
IMachine immutableMachine = manager.get().getVBox().findMachine(machineId);
|
||||
|
||||
try {
|
||||
session = manager.get().openMachineSession(immutableMachine);
|
||||
if (session.getState().equals(SessionState.Locked))
|
||||
return checkNotNull(session, "session");
|
||||
} catch (Exception e) {
|
||||
logger.debug("machine %s is not locked). Error: %s", immutableMachine.getName(), e.getMessage());
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
IMachine immutableMachine = manager.get().getVBox().findMachine(machineId);
|
||||
session = manager.get().getSessionObject();
|
||||
immutableMachine.lockMachine(session, type);
|
||||
break;
|
||||
|
@ -219,13 +232,13 @@ public class MachineUtils {
|
|||
return null;
|
||||
}
|
||||
count++;
|
||||
logger.warn(e, "Could not lock machine (try %d of %d). Error: %s", count, retries, e.getMessage());
|
||||
logger.debug("Could not lock machine (try %d of %d). Error: %s", count, retries, e.getMessage());
|
||||
if (count == retries) {
|
||||
throw new RuntimeException(String.format("error locking %s with %s lock: %s", machineId, type,
|
||||
e.getMessage()), e);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000L);
|
||||
Thread.sleep(count * 1000L);
|
||||
} catch (InterruptedException e1) {
|
||||
}
|
||||
}
|
||||
|
@ -265,70 +278,24 @@ public class MachineUtils {
|
|||
|| e.getMessage().contains("Could not find a registered machine with UUID {");
|
||||
}
|
||||
|
||||
public String getIpAddressFromBridgedNIC(String machineName) {
|
||||
String ip = "";
|
||||
int attempt = 0;
|
||||
while (!isIpv4(ip) && attempt < 10) {
|
||||
ip = this.lockSessionOnMachineAndApply(machineName, LockType.Shared, new Function<ISession, String>() {
|
||||
@Override
|
||||
public String apply(ISession session) {
|
||||
String ip = session.getMachine().getGuestPropertyValue("/VirtualBox/GuestInfo/Net/0/V4/IP");
|
||||
return ip;
|
||||
}
|
||||
});
|
||||
attempt++;
|
||||
long sleepTime = 1000 * attempt;
|
||||
logger.debug("Instance %s is still not ready. Attempt n:%d. Sleeping for %d millisec", machineName, attempt,
|
||||
sleepTime);
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
} catch (InterruptedException e) {
|
||||
Throwables.propagate(e);
|
||||
}
|
||||
public String getIpAddressFromFirstNIC(String machineName) {
|
||||
try {
|
||||
return ipAddressesLoadingCache.get(machineName);
|
||||
} catch (ExecutionException e) {
|
||||
logger.error("Problem in using the ipAddressCache", e.getCause());
|
||||
throw Throwables.propagate(e);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
private boolean isIpv4(String s) {
|
||||
Pattern pattern = Pattern.compile(this.IP_V4_ADDRESS_PATTERN);
|
||||
|
||||
public static boolean isIpv4(String s) {
|
||||
String IP_V4_ADDRESS_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
|
||||
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
|
||||
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
|
||||
Pattern pattern = Pattern.compile(IP_V4_ADDRESS_PATTERN);
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
return matcher.matches();
|
||||
}
|
||||
|
||||
public String getIpAddressFromHostOnlyNIC(String machineName) {
|
||||
// TODO using a caching mechanism to avoid to call every time this vboxmanage api call
|
||||
String currentIp = "", previousIp = "1.1.1.1";
|
||||
int attempt = 0, count = 0;
|
||||
while(count < 5) {
|
||||
currentIp = "";
|
||||
attempt = 0;
|
||||
while (!isIpv4(currentIp) && attempt < 5) {
|
||||
currentIp = this.lockSessionOnMachineAndApply(machineName, LockType.Shared, new Function<ISession, String>() {
|
||||
@Override
|
||||
public String apply(ISession session) {
|
||||
return session.getMachine().getGuestPropertyValue("/VirtualBox/GuestInfo/Net/0/V4/IP");
|
||||
}
|
||||
});
|
||||
attempt++;
|
||||
}
|
||||
if(previousIp.equals(currentIp)) {
|
||||
count++;
|
||||
delayer(500l * (count + 1));
|
||||
} else {
|
||||
count = 0;
|
||||
delayer(5000l);
|
||||
}
|
||||
previousIp = currentIp;
|
||||
}
|
||||
return currentIp;
|
||||
}
|
||||
|
||||
private void delayer(long millisec) {
|
||||
try {
|
||||
Thread.sleep(millisec);
|
||||
} catch (InterruptedException e) {
|
||||
Throwables.propagate(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -133,3 +133,90 @@ images:
|
|||
# debconf-get-selections --install
|
||||
#Use mirror
|
||||
choose-mirror-bin mirror/http/proxy string
|
||||
- id: ubuntu-12.04.1-amd64
|
||||
name: ubuntu-12.04.1-server-amd64
|
||||
description: ubuntu 12.04.1 server (amd64)
|
||||
os_arch: amd64
|
||||
os_family: ubuntu
|
||||
os_description: ubuntu
|
||||
os_version: 12.04.1
|
||||
os_64bit: true
|
||||
iso: http://releases.ubuntu.com/12.04/ubuntu-12.04.1-server-amd64.iso
|
||||
username: toor
|
||||
credential: $user
|
||||
keystroke_sequence: |
|
||||
<Esc><Esc><Enter>
|
||||
/install/vmlinuz noapic preseed/url=http://10.0.2.2:8080/src/test/resources/preseed.cfg
|
||||
debian-installer=en_US auto locale=en_US kbd-chooser/method=us
|
||||
hostname=vmName
|
||||
fb=false debconf/frontend=noninteractive
|
||||
keyboard-configuration/layout=USA keyboard-configuration/variant=USA console-setup/ask_detect=false
|
||||
initrd=/install/initrd.gz -- <Enter>
|
||||
preseed_cfg: |
|
||||
## Options to set on the command line
|
||||
d-i debian-installer/locale string en_US.utf8
|
||||
d-i console-setup/ask_detect boolean false
|
||||
d-i console-setup/layout string USA
|
||||
#d-i netcfg/get_hostname string dummy
|
||||
d-i netcfg/get_hostname string unassigned-hostname
|
||||
d-i netcfg/get_domain string unassigned-domain
|
||||
# Continue without a default route
|
||||
# Not working , specify a dummy in the DHCP
|
||||
#d-i netcfg/no_default_route boolean
|
||||
d-i time/zone string UTC
|
||||
d-i clock-setup/utc-auto boolean true
|
||||
d-i clock-setup/utc boolean true
|
||||
d-i kbd-chooser/method select American English
|
||||
d-i netcfg/wireless_wep string
|
||||
d-i base-installer/kernel/override-image string linux-server
|
||||
#d-i base-installer/kernel/override-image string linux-image-2.6.32-21-generic
|
||||
# Choices: Dialog, Readline, Gnome, Kde, Editor, Noninteractive
|
||||
d-i debconf debconf/frontend select Noninteractive
|
||||
d-i pkgsel/install-language-support boolean false
|
||||
tasksel tasksel/first multiselect standard, ubuntu-server
|
||||
#d-i partman-auto/method string regular
|
||||
d-i partman-auto/method string lvm
|
||||
#d-i partman-auto/purge_lvm_from_device boolean true
|
||||
d-i partman-lvm/confirm boolean true
|
||||
d-i partman-lvm/device_remove_lvm boolean true
|
||||
d-i partman-auto/choose_recipe select atomic
|
||||
d-i partman/confirm_write_new_label boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
#http://ubuntu-virginia.ubuntuforums.org/showthread.php?p=9626883
|
||||
#Message: "write the changes to disk and configure lvm preseed"
|
||||
#http://serverfault.com/questions/189328/ubuntu-kickstart-installation-using-lvm-waits-for-input
|
||||
#preseed partman-lvm/confirm_nooverwrite boolean true
|
||||
# Write the changes to disks and configure LVM?
|
||||
d-i partman-lvm/confirm boolean true
|
||||
d-i partman-lvm/confirm_nooverwrite boolean true
|
||||
d-i partman-auto-lvm/guided_size string max
|
||||
## Default user, we can get away with a recipe to change this
|
||||
d-i passwd/user-fullname string toor
|
||||
d-i passwd/username string toor
|
||||
d-i passwd/user-password password password
|
||||
d-i passwd/user-password-again password password
|
||||
d-i user-setup/encrypt-home boolean false
|
||||
d-i user-setup/allow-password-weak boolean true
|
||||
## minimum is ssh and ntp
|
||||
# Individual additional packages to install
|
||||
d-i pkgsel/include string openssh-server ntp
|
||||
# Whether to upgrade packages after debootstrap.
|
||||
# Allowed values: none, safe-upgrade, full-upgrade
|
||||
d-i pkgsel/upgrade select full-upgrade
|
||||
d-i grub-installer/only_debian boolean true
|
||||
d-i grub-installer/with_other_os boolean true
|
||||
d-i finish-install/reboot_in_progress note
|
||||
#For the update
|
||||
d-i pkgsel/update-policy select none
|
||||
# debconf-get-selections --install
|
||||
#Use mirror
|
||||
#d-i apt-setup/use_mirror boolean true
|
||||
#d-i mirror/country string manual
|
||||
#choose-mirror-bin mirror/protocol string http
|
||||
#choose-mirror-bin mirror/http/hostname string 192.168.4.150
|
||||
#choose-mirror-bin mirror/http/directory string /ubuntu
|
||||
#choose-mirror-bin mirror/suite select maverick
|
||||
#d-i debian-installer/allow_unauthenticated string true
|
||||
choose-mirror-bin mirror/http/proxy string
|
|
@ -119,7 +119,7 @@ public class BaseVirtualBoxClientLiveTest extends BaseComputeServiceContextLiveT
|
|||
protected LoadingCache<Image, Master> mastersCache;
|
||||
|
||||
private final ExecutorService singleThreadExec = MoreExecutors.sameThreadExecutor();
|
||||
private String masterVmName;
|
||||
private String masterName;
|
||||
|
||||
|
||||
@Override
|
||||
|
@ -139,7 +139,7 @@ public class BaseVirtualBoxClientLiveTest extends BaseComputeServiceContextLiveT
|
|||
Template template = view.getComputeService().templateBuilder().build();
|
||||
checkNotNull(mastersCache.apply(template.getImage()));
|
||||
|
||||
masterVmName = VIRTUALBOX_IMAGE_PREFIX + template.getImage().getId();
|
||||
masterName = VIRTUALBOX_IMAGE_PREFIX + template.getImage().getId();
|
||||
isosDir = workingDir + File.separator + "isos";
|
||||
|
||||
hostVersion = Iterables.get(Splitter.on('r').split(view.utils().injector().getInstance(Key.get(String.class, BuildVersion.class))), 0);
|
||||
|
@ -173,8 +173,6 @@ public class BaseVirtualBoxClientLiveTest extends BaseComputeServiceContextLiveT
|
|||
}
|
||||
|
||||
public MasterSpec getMasterSpecForTest() {
|
||||
String masterName = "jclouds-image-0x0-" + template.getImageId();
|
||||
|
||||
StorageController ideController = StorageController
|
||||
.builder()
|
||||
.name("IDE Controller")
|
||||
|
@ -220,7 +218,7 @@ public class BaseVirtualBoxClientLiveTest extends BaseComputeServiceContextLiveT
|
|||
protected void destroyMaster() {
|
||||
if (System.getProperty(DONT_DESTROY_MASTER) == null
|
||||
|| !Boolean.parseBoolean(System.getProperty(DONT_DESTROY_MASTER))) {
|
||||
undoVm(masterVmName);
|
||||
undoVm(masterName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ package org.jclouds.virtualbox;
|
|||
|
||||
import static org.testng.Assert.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
|
|
|
@ -51,7 +51,7 @@ public class VirtualBoxComputeServiceAdapterLiveTest extends BaseVirtualBoxClien
|
|||
@Test
|
||||
public void testCreatedNodeHasExpectedNameAndWeCanConnectViaSsh() {
|
||||
String group = "foo";
|
||||
String name = "foo-ef4";
|
||||
String name = "foo-ef9";
|
||||
Template template = view.getComputeService().templateBuilder().build();
|
||||
machine = adapter.createNodeWithGroupEncodedIntoName(group, name, template);
|
||||
assertTrue(machine.getNode().getName().contains(group));
|
||||
|
|
|
@ -36,7 +36,6 @@ import org.jclouds.compute.options.TemplateOptions;
|
|||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.logging.Logger;
|
||||
import org.jclouds.logging.slf4j.config.SLF4JLoggingModule;
|
||||
import org.jclouds.scriptbuilder.statements.login.AdminAccess;
|
||||
import org.jclouds.ssh.SshClient;
|
||||
import org.jclouds.sshj.config.SshjSshClientModule;
|
||||
import org.jclouds.virtualbox.BaseVirtualBoxClientLiveTest;
|
||||
|
@ -69,10 +68,10 @@ public class VirtualBoxExperimentLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
|
||||
@Test
|
||||
public void testLaunchCluster() throws RunNodesException {
|
||||
int numNodes = 2;
|
||||
int numNodes = 3;
|
||||
final String clusterName = "test-launch-cluster";
|
||||
Set<? extends NodeMetadata> nodes = context.getComputeService().createNodesInGroup(clusterName, numNodes,
|
||||
TemplateOptions.Builder.runScript(AdminAccess.standard()));
|
||||
TemplateOptions.Builder.overrideLoginUser("toor")); //TODO runScript(AdminAccess.standard()));
|
||||
assertEquals(numNodes, nodes.size(), "wrong number of nodes");
|
||||
for (NodeMetadata node : nodes) {
|
||||
assertTrue(node.getGroup().equals("test-launch-cluster"));
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
package org.jclouds.virtualbox.compute.extensions;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.jclouds.compute.RunNodesException;
|
||||
import org.jclouds.compute.extensions.internal.BaseImageExtensionLiveTest;
|
||||
import org.jclouds.sshj.config.SshjSshClientModule;
|
||||
import org.testng.annotations.Test;
|
||||
|
@ -28,6 +31,22 @@ import com.google.inject.Module;
|
|||
@Test(groups = "live", singleThreaded = true, testName = "VirtualBoxImageExtensionLiveTest")
|
||||
public class VirtualBoxImageExtensionLiveTest extends BaseImageExtensionLiveTest {
|
||||
|
||||
@Override
|
||||
public void testDeleteImage() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testCreateImage() throws RunNodesException,
|
||||
InterruptedException, ExecutionException {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void testSpawnNodeFromImage() throws RunNodesException {
|
||||
// TODO
|
||||
}
|
||||
|
||||
public VirtualBoxImageExtensionLiveTest() {
|
||||
provider = "virtualbox";
|
||||
}
|
||||
|
|
|
@ -24,14 +24,18 @@ import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_IMAGE
|
|||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_INSTALLATION_KEY_SEQUENCE;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.jclouds.compute.config.BaseComputeServiceContextModule;
|
||||
import org.jclouds.compute.domain.OsFamily;
|
||||
import org.jclouds.compute.reference.ComputeServiceConstants;
|
||||
import org.jclouds.config.ValueOfConfigurationKeyOrNull;
|
||||
import org.jclouds.json.Json;
|
||||
import org.jclouds.json.config.GsonModule;
|
||||
import org.jclouds.location.Provider;
|
||||
import org.jclouds.ssh.SshClient;
|
||||
import org.jclouds.virtualbox.BaseVirtualBoxClientLiveTest;
|
||||
import org.jclouds.virtualbox.domain.CloneSpec;
|
||||
|
@ -49,13 +53,13 @@ import org.testng.annotations.BeforeClass;
|
|||
import org.testng.annotations.Test;
|
||||
import org.virtualbox_4_1.CleanupMode;
|
||||
import org.virtualbox_4_1.IMachine;
|
||||
import org.virtualbox_4_1.ISession;
|
||||
import org.virtualbox_4_1.NetworkAttachmentType;
|
||||
import org.virtualbox_4_1.StorageBus;
|
||||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
|
@ -77,42 +81,9 @@ public class CreateAndInstallVmLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
private MasterSpec machineSpec;
|
||||
private String instanceName;
|
||||
|
||||
/*
|
||||
@Override
|
||||
@BeforeClass(groups = "live")
|
||||
public void setupClient() {
|
||||
super.setupClient();
|
||||
this.vmName = VIRTUALBOX_IMAGE_PREFIX
|
||||
+ CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, getClass().getSimpleName());
|
||||
|
||||
HardDisk hardDisk = HardDisk.builder().diskpath(adminDisk(vmName)).autoDelete(true).controllerPort(0)
|
||||
.deviceSlot(1).build();
|
||||
StorageController ideController = StorageController.builder().name("IDE Controller").bus(StorageBus.IDE)
|
||||
.attachISO(0, 0, operatingSystemIso).attachHardDisk(hardDisk).attachISO(1, 1, guestAdditionsIso).build();
|
||||
vmSpecification = VmSpec.builder().id(vmName).name(vmName).memoryMB(512).osTypeId("").controller(ideController)
|
||||
.forceOverwrite(true).cleanUpMode(CleanupMode.Full).build();
|
||||
|
||||
injector = context.utils().injector();
|
||||
Function<String, String> configProperties = injector.getInstance(ValueOfConfigurationKeyOrNull.class);
|
||||
|
||||
NetworkAdapter natAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.tcpRedirectRule("127.0.0.1", 2222, "", 22).build();
|
||||
|
||||
NetworkInterfaceCard networkInterfaceCard1 = NetworkInterfaceCard.builder().slot(0l)
|
||||
.addNetworkAdapter(natAdapter).build();
|
||||
|
||||
NetworkSpec networkSpec = NetworkSpec.builder().addNIC(networkInterfaceCard1).build();
|
||||
|
||||
masterSpec = MasterSpec
|
||||
.builder()
|
||||
.vm(vmSpecification)
|
||||
.iso(IsoSpec
|
||||
.builder()
|
||||
.sourcePath(operatingSystemIso)
|
||||
.installationScript(
|
||||
configProperties.apply(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE).replace("HOSTNAME",
|
||||
vmSpecification.getVmName())).build()).network(networkSpec).build();
|
||||
}*/
|
||||
@Inject
|
||||
@Provider
|
||||
protected Supplier<URI> providerSupplier;
|
||||
|
||||
@Override
|
||||
@BeforeClass(groups = "live")
|
||||
|
@ -142,11 +113,10 @@ public class CreateAndInstallVmLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
configProperties.apply(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE).replace("HOSTNAME",
|
||||
instanceVmSpec.getVmName())).build();
|
||||
|
||||
NetworkAdapter networkAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.tcpRedirectRule("127.0.0.1", 2222, "", 22).build();
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(networkAdapter)
|
||||
NetworkAdapter networkAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.HostOnly)
|
||||
.build();
|
||||
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(networkAdapter)
|
||||
.addHostInterfaceName("vboxnet0").slot(0L).build();
|
||||
NetworkSpec networkSpec = NetworkSpec.builder().addNIC(networkInterfaceCard).build();
|
||||
machineSpec = MasterSpec.builder().iso(isoSpec).vm(instanceVmSpec).network(networkSpec).build();
|
||||
}
|
||||
|
@ -164,12 +134,7 @@ public class CreateAndInstallVmLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
checkState(sshResponds.apply(client), "timed out waiting for guest %s to be accessible via ssh",
|
||||
machine.getName());
|
||||
|
||||
String version = machineUtils.sharedLockMachineAndApplyToSession(machine.getName(), new Function<ISession, String>() {
|
||||
@Override
|
||||
public String apply(ISession session) {
|
||||
return session.getMachine().getGuestPropertyValue("/VirtualBox/GuestAdd/Version");
|
||||
}
|
||||
});
|
||||
String version = machine.getGuestPropertyValue("/VirtualBox/GuestAdd/Version");
|
||||
|
||||
assertTrue(version != null && !version.isEmpty());
|
||||
} finally {
|
||||
|
@ -193,7 +158,6 @@ public class CreateAndInstallVmLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
Injector injector = view.utils().injector();
|
||||
return injector.getInstance(CreateAndInstallVm.class).apply(masterSpecForTest);
|
||||
} catch (IllegalStateException e) {
|
||||
// already created
|
||||
return manager.get().getVBox().findMachine(masterSpecForTest.getVmSpec().getVmId());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ public class StartVBoxIfNotAlreadyRunningLiveTest {
|
|||
replay(manager, runScriptOnNodeFactory, client);
|
||||
|
||||
new StartVBoxIfNotAlreadyRunning((Function) Functions.constant(manager), runScriptOnNodeFactory, client,
|
||||
Suppliers.ofInstance(host), Suppliers.ofInstance(provider)).start();
|
||||
Suppliers.ofInstance(host), Suppliers.ofInstance(provider), null).start();
|
||||
|
||||
verify(manager, runScriptOnNodeFactory, client);
|
||||
|
||||
|
@ -84,26 +84,26 @@ public class StartVBoxIfNotAlreadyRunningLiveTest {
|
|||
|
||||
expect(client.seconds(3)).andReturn(client);
|
||||
expect(client.apply(HostAndPort.fromParts(provider.getHost(), provider.getPort()))).andReturn(false).once().andReturn(true).once();
|
||||
expect(
|
||||
runScriptOnNodeFactory.create(host, Statements.exec("VBoxManage setproperty websrvauthlibrary null"),
|
||||
expect(runScriptOnNodeFactory.create(host,
|
||||
Statements.exec("VBoxManage setproperty websrvauthlibrary null"),
|
||||
runAsRoot(false).wrapInInitScript(false))).andReturn(runScriptOnNode);
|
||||
expect(runScriptOnNode.init()).andReturn(runScriptOnNode);
|
||||
expect(runScriptOnNode.call()).andReturn(new ExecResponse("", "", 0));
|
||||
|
||||
expect(
|
||||
runScriptOnNodeFactory.create(host, Statements.exec("vboxwebsrv -t 10000 -v -b"), runAsRoot(false)
|
||||
.wrapInInitScript(false).blockOnComplete(false).nameTask("vboxwebsrv"))).andReturn(
|
||||
runScriptOnNode);
|
||||
expect(runScriptOnNodeFactory.create(host,
|
||||
Statements.exec("vboxwebsrv -t 10000 -v -b -H localhost"), runAsRoot(false)
|
||||
.wrapInInitScript(false).blockOnComplete(false).nameTask("vboxwebsrv")))
|
||||
.andReturn(runScriptOnNode);
|
||||
|
||||
expect(runScriptOnNode.init()).andReturn(runScriptOnNode);
|
||||
expect(runScriptOnNode.call()).andReturn(new ExecResponse("", "", 0));
|
||||
|
||||
manager.connect(provider.toASCIIString(), "", "");
|
||||
expectLastCall().anyTimes();
|
||||
|
||||
replay(manager, runScriptOnNodeFactory, runScriptOnNode, client);
|
||||
new StartVBoxIfNotAlreadyRunning((Function) Functions.constant(manager), runScriptOnNodeFactory, client,
|
||||
Suppliers.ofInstance(host), Suppliers.ofInstance(provider));
|
||||
verify(manager, runScriptOnNodeFactory, runScriptOnNode, client);
|
||||
Suppliers.ofInstance(host), Suppliers.ofInstance(provider), null);
|
||||
|
||||
verify(manager, runScriptOnNodeFactory, runScriptOnNode, client);
|
||||
}
|
||||
}
|
|
@ -24,9 +24,6 @@ import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_IMAGE
|
|||
import static org.jclouds.virtualbox.config.VirtualBoxConstants.VIRTUALBOX_INSTALLATION_KEY_SEQUENCE;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jclouds.config.ValueOfConfigurationKeyOrNull;
|
||||
import org.jclouds.ssh.SshClient;
|
||||
import org.jclouds.virtualbox.BaseVirtualBoxClientLiveTest;
|
||||
|
@ -42,11 +39,12 @@ import org.jclouds.virtualbox.domain.VmSpec;
|
|||
import org.jclouds.virtualbox.functions.CloneAndRegisterMachineFromIMachineIfNotAlreadyExists;
|
||||
import org.jclouds.virtualbox.functions.CreateAndInstallVm;
|
||||
import org.jclouds.virtualbox.functions.IMachineToSshClient;
|
||||
import org.jclouds.virtualbox.functions.IpAddressesLoadingCache;
|
||||
import org.jclouds.virtualbox.util.MachineUtils;
|
||||
import org.testng.annotations.BeforeClass;
|
||||
import org.testng.annotations.Test;
|
||||
import org.virtualbox_4_1.CleanupMode;
|
||||
import org.virtualbox_4_1.IMachine;
|
||||
import org.virtualbox_4_1.ISession;
|
||||
import org.virtualbox_4_1.NetworkAttachmentType;
|
||||
import org.virtualbox_4_1.StorageBus;
|
||||
|
||||
|
@ -67,6 +65,7 @@ public class GuestAdditionsInstallerLiveTest extends BaseVirtualBoxClientLiveTes
|
|||
private Predicate<SshClient> sshResponds;
|
||||
|
||||
private MasterSpec machineSpec;
|
||||
private IpAddressesLoadingCache ipAddressesLoadingCache;
|
||||
|
||||
@Override
|
||||
@BeforeClass(groups = "live")
|
||||
|
@ -118,21 +117,10 @@ public class GuestAdditionsInstallerLiveTest extends BaseVirtualBoxClientLiveTes
|
|||
sshResponds = injector.getInstance(SshResponds.class);
|
||||
checkState(sshResponds.apply(client), "timed out waiting for guest %s to be accessible via ssh",
|
||||
machine.getName());
|
||||
ipAddressesLoadingCache = injector.getInstance(IpAddressesLoadingCache.class);
|
||||
|
||||
assertTrue(machineUtils.sharedLockMachineAndApplyToSession(machine.getName(),
|
||||
new Function<ISession, Boolean>() {
|
||||
@Override
|
||||
public Boolean apply(ISession session) {
|
||||
String s = session.getMachine().getGuestPropertyValue("/VirtualBox/GuestInfo/Net/0/V4/IP");
|
||||
return isIpv4(s);
|
||||
}
|
||||
assertTrue(MachineUtils.isIpv4(ipAddressesLoadingCache.apply(machine.getName())));
|
||||
|
||||
private boolean isIpv4(String s) {
|
||||
Pattern pattern = Pattern.compile(machineUtils.IP_V4_ADDRESS_PATTERN);
|
||||
Matcher matcher = pattern.matcher(s);
|
||||
return matcher.matches();
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
for (String vmNameOrId : ImmutableSet.of(machine.getName())) {
|
||||
machineController.ensureMachineHasPowerDown(vmNameOrId);
|
||||
|
|
|
@ -77,21 +77,25 @@ public class MachineControllerLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
.cleanUpMode(CleanupMode.Full).controller(ideController).forceOverwrite(true).build();
|
||||
|
||||
Injector injector = view.utils().injector();
|
||||
Function<String, String> configProperties = injector.getInstance(ValueOfConfigurationKeyOrNull.class);
|
||||
Function<String, String> configProperties = injector
|
||||
.getInstance(ValueOfConfigurationKeyOrNull.class);
|
||||
IsoSpec isoSpec = IsoSpec
|
||||
.builder()
|
||||
.sourcePath(operatingSystemIso)
|
||||
.installationScript(
|
||||
configProperties.apply(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE).replace("HOSTNAME",
|
||||
instanceVmSpec.getVmName())).build();
|
||||
.builder()
|
||||
.sourcePath(operatingSystemIso)
|
||||
.installationScript(
|
||||
configProperties.apply(VIRTUALBOX_INSTALLATION_KEY_SEQUENCE)
|
||||
.replace("HOSTNAME", instanceVmSpec.getVmName()))
|
||||
.build();
|
||||
|
||||
NetworkAdapter networkAdapter = NetworkAdapter.builder().networkAttachmentType(NetworkAttachmentType.NAT)
|
||||
.tcpRedirectRule("127.0.0.1", 2222, "", 22).build();
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard.builder().addNetworkAdapter(networkAdapter)
|
||||
.build();
|
||||
|
||||
NetworkSpec networkSpec = NetworkSpec.builder().addNIC(networkInterfaceCard).build();
|
||||
machineSpec = MasterSpec.builder().iso(isoSpec).vm(instanceVmSpec).network(networkSpec).build();
|
||||
NetworkAdapter networkAdapter = NetworkAdapter.builder()
|
||||
.networkAttachmentType(NetworkAttachmentType.HostOnly).build();
|
||||
NetworkInterfaceCard networkInterfaceCard = NetworkInterfaceCard
|
||||
.builder().addNetworkAdapter(networkAdapter)
|
||||
.addHostInterfaceName("vboxnet0").slot(0L).build();
|
||||
NetworkSpec networkSpec = NetworkSpec.builder()
|
||||
.addNIC(networkInterfaceCard).build();
|
||||
machineSpec = MasterSpec.builder().iso(isoSpec).vm(instanceVmSpec)
|
||||
.network(networkSpec).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -106,8 +110,8 @@ public class MachineControllerLiveTest extends BaseVirtualBoxClientLiveTest {
|
|||
@Test(dependsOnMethods="testEnsureMachineisLaunchedAndSessionIsUnlocked")
|
||||
public void testEnsureMachineCanBePoweredOffMoreThanOneTimeAndSessionIsUnlocked() {
|
||||
ISession cloneMachineSession = machineController.ensureMachineHasPowerDown(instanceName);
|
||||
cloneMachineSession = machineController.ensureMachineHasPowerDown(instanceName);
|
||||
assertTrue(cloneMachineSession.getState() == SessionState.Unlocked);
|
||||
SessionState state = cloneMachineSession.getState();
|
||||
assertTrue(state.equals(SessionState.Unlocked));
|
||||
}
|
||||
|
||||
private IMachine cloneFromMaster() {
|
||||
|
|
|
@ -66,7 +66,7 @@ images:
|
|||
# debconf-get-selections --install
|
||||
#Use mirror
|
||||
choose-mirror-bin mirror/http/proxy string
|
||||
- id: test-ubuntu-11.10-i386
|
||||
- id: ubuntu-11.10-i386
|
||||
name: ubuntu-11.10-server-i386
|
||||
description: ubuntu 11.10 server (i386)
|
||||
os_arch: x86
|
||||
|
@ -133,3 +133,89 @@ images:
|
|||
# debconf-get-selections --install
|
||||
#Use mirror
|
||||
choose-mirror-bin mirror/http/proxy string
|
||||
- id: ubuntu-12.04.1-amd64
|
||||
name: ubuntu-12.04.1-server-amd64
|
||||
description: ubuntu 12.04.1 server (amd64)
|
||||
os_arch: amd64
|
||||
os_family: ubuntu
|
||||
os_description: ubuntu
|
||||
os_version: 12.04.1
|
||||
os_64bit: true
|
||||
iso: http://releases.ubuntu.com/12.04/ubuntu-12.04.1-server-amd64.iso
|
||||
iso_md5: a8c667e871f48f3a662f3fbf1c3ddb17
|
||||
keystroke_sequence: |
|
||||
<Esc><Esc><Enter>
|
||||
/install/vmlinuz noapic preseed/url=http://10.0.2.2:8080/src/test/resources/preseed.cfg
|
||||
debian-installer=en_US auto locale=en_US kbd-chooser/method=us
|
||||
hostname=vmName
|
||||
fb=false debconf/frontend=noninteractive
|
||||
keyboard-configuration/layout=USA keyboard-configuration/variant=USA console-setup/ask_detect=false
|
||||
initrd=/install/initrd.gz -- <Enter>
|
||||
preseed_cfg: |
|
||||
## Options to set on the command line
|
||||
d-i debian-installer/locale string en_US.utf8
|
||||
d-i console-setup/ask_detect boolean false
|
||||
d-i console-setup/layout string USA
|
||||
#d-i netcfg/get_hostname string dummy
|
||||
d-i netcfg/get_hostname string unassigned-hostname
|
||||
d-i netcfg/get_domain string unassigned-domain
|
||||
# Continue without a default route
|
||||
# Not working , specify a dummy in the DHCP
|
||||
#d-i netcfg/no_default_route boolean
|
||||
d-i time/zone string UTC
|
||||
d-i clock-setup/utc-auto boolean true
|
||||
d-i clock-setup/utc boolean true
|
||||
d-i kbd-chooser/method select American English
|
||||
d-i netcfg/wireless_wep string
|
||||
d-i base-installer/kernel/override-image string linux-server
|
||||
#d-i base-installer/kernel/override-image string linux-image-2.6.32-21-generic
|
||||
# Choices: Dialog, Readline, Gnome, Kde, Editor, Noninteractive
|
||||
d-i debconf debconf/frontend select Noninteractive
|
||||
d-i pkgsel/install-language-support boolean false
|
||||
tasksel tasksel/first multiselect standard, ubuntu-server
|
||||
#d-i partman-auto/method string regular
|
||||
d-i partman-auto/method string lvm
|
||||
#d-i partman-auto/purge_lvm_from_device boolean true
|
||||
d-i partman-lvm/confirm boolean true
|
||||
d-i partman-lvm/device_remove_lvm boolean true
|
||||
d-i partman-auto/choose_recipe select atomic
|
||||
d-i partman/confirm_write_new_label boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
#http://ubuntu-virginia.ubuntuforums.org/showthread.php?p=9626883
|
||||
#Message: "write the changes to disk and configure lvm preseed"
|
||||
#http://serverfault.com/questions/189328/ubuntu-kickstart-installation-using-lvm-waits-for-input
|
||||
#preseed partman-lvm/confirm_nooverwrite boolean true
|
||||
# Write the changes to disks and configure LVM?
|
||||
d-i partman-lvm/confirm boolean true
|
||||
d-i partman-lvm/confirm_nooverwrite boolean true
|
||||
d-i partman-auto-lvm/guided_size string max
|
||||
## Default user, we can get away with a recipe to change this
|
||||
d-i passwd/user-fullname string toor
|
||||
d-i passwd/username string toor
|
||||
d-i passwd/user-password password password
|
||||
d-i passwd/user-password-again password password
|
||||
d-i user-setup/encrypt-home boolean false
|
||||
d-i user-setup/allow-password-weak boolean true
|
||||
## minimum is ssh and ntp
|
||||
# Individual additional packages to install
|
||||
d-i pkgsel/include string openssh-server ntp
|
||||
# Whether to upgrade packages after debootstrap.
|
||||
# Allowed values: none, safe-upgrade, full-upgrade
|
||||
d-i pkgsel/upgrade select full-upgrade
|
||||
d-i grub-installer/only_debian boolean true
|
||||
d-i grub-installer/with_other_os boolean true
|
||||
d-i finish-install/reboot_in_progress note
|
||||
#For the update
|
||||
d-i pkgsel/update-policy select none
|
||||
# debconf-get-selections --install
|
||||
#Use mirror
|
||||
#d-i apt-setup/use_mirror boolean true
|
||||
#d-i mirror/country string manual
|
||||
#choose-mirror-bin mirror/protocol string http
|
||||
#choose-mirror-bin mirror/http/hostname string 192.168.4.150
|
||||
#choose-mirror-bin mirror/http/directory string /ubuntu
|
||||
#choose-mirror-bin mirror/suite select maverick
|
||||
#d-i debian-installer/allow_unauthenticated string true
|
||||
choose-mirror-bin mirror/http/proxy string
|
Loading…
Reference in New Issue