/** * Copyright (C) 2009-2015 Dell, Inc * See annotations for authorship information * * ==================================================================== * Licensed 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.dasein.cloud.joyent.compute; import org.dasein.cloud.AsynchronousTask; import org.dasein.cloud.CloudErrorType; import org.dasein.cloud.CloudException; import org.dasein.cloud.InternalException; import org.dasein.cloud.ProviderContext; import org.dasein.cloud.compute.*; import org.dasein.cloud.identity.ServiceAction; import org.dasein.cloud.joyent.JoyentException; import org.dasein.cloud.joyent.JoyentMethod; import org.dasein.cloud.joyent.SmartDataCenter; import org.dasein.cloud.util.APITrace; import org.dasein.cloud.util.CacheLevel; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; import java.util.regex.Pattern; public class Dataset extends AbstractImageSupport<SmartDataCenter> { private static final String[] VALID_CAPTURE_OS = { "base", "base64", "centos-6", "debian-7", "ubuntu-certified-14.04", "ubuntu-certified-13.10", "ubuntu-certified-12.04" }; private static final String OWNER_JOYENT = "--joyent--"; private volatile transient DatasetCapabilities capabilities; Dataset( @Nonnull SmartDataCenter sdc ) { super(sdc); } @Override public ImageCapabilities getCapabilities() throws CloudException, InternalException { if( capabilities == null ) { capabilities = new DatasetCapabilities(getProvider()); } return capabilities; } @Override protected MachineImage capture( @Nonnull ImageCreateOptions options, @Nullable AsynchronousTask<MachineImage> task ) throws CloudException, InternalException { APITrace.begin(getProvider(), "Image.capture"); try { VirtualMachine vm = getProvider().getComputeServices().getVirtualMachineSupport().getVirtualMachine(options.getVirtualMachineId()); if( vm == null ) { throw new CloudException("Virtual machine not found: " + options.getVirtualMachineId()); } String originalImageId = vm.getProviderMachineImageId(); if( originalImageId == null ) { throw new CloudException("Image capture is not supported for this VM, original image is unknown."); } MachineImage originalImage = getImage(originalImageId); if( !OWNER_JOYENT.equalsIgnoreCase(originalImage.getProviderOwnerId()) ) { throw new CloudException("Image capture is not supported for VMs launched from custom images."); } boolean validOs = false; for( String os : VALID_CAPTURE_OS ) { if( originalImage.getName().startsWith(os) ) { validOs = true; break; } } if( !validOs ) { throw new CloudException("Image capture is not supported for this VM, the OS of the original image is not supported."); } if( task != null ) { task.setStartTime(System.currentTimeMillis()); } String vmID = options.getVirtualMachineId(); String imageName = options.getName(); String version = "1.0.0"; if( !getCapabilities().canImage(vm.getCurrentState()) ) { throw new CloudException("Server must be stopped before making an image - current state: " + vm.getCurrentState()); } JoyentMethod method = new JoyentMethod(getProvider()); Map<String, Object> post = new HashMap<String, Object>(); post.put("machine", vmID); post.put("name", imageName); post.put("version", version); String json = method.doPostString(getProvider().getEndpoint(), "images", new JSONObject(post).toString()); if( json == null ) { throw new CloudException("No image was created"); } MachineImage img = null; try { JSONObject jsonObject = new JSONObject(json); if( jsonObject.has("id") ) { img = getImage(jsonObject.getString("id")); } } catch( JSONException e ) { throw new CloudException(e); } if( img == null ) { throw new CloudException("No image was created"); } if( task != null ) { task.completeWithResult(img); } return img; } finally { APITrace.end(); } } @Override public MachineImage getImage( @Nonnull String providerImageId ) throws CloudException, InternalException { JoyentMethod method = new JoyentMethod(getProvider()); try { String json = method.doGetJson(getProvider().getEndpoint(), "images/" + providerImageId); if( json == null ) { return null; } return toMachineImage(new JSONObject(json)); } catch( JSONException e ) { throw new CloudException(e); } } @Override public boolean isImageSharedWithPublic( @Nonnull String machineImageId ) throws CloudException, InternalException { JoyentMethod method = new JoyentMethod(getProvider()); try { String json = method.doGetJson(getProvider().getEndpoint(), "images/" + machineImageId); if( json != null ) { JSONObject jsonObject = new JSONObject(json); if( jsonObject.has("public") ) { return jsonObject.getBoolean("public"); } } return false; } catch( JSONException e ) { throw new CloudException(e); } } @Override public boolean isSubscribed() throws CloudException, InternalException { org.dasein.cloud.util.Cache<Boolean> cache = org.dasein.cloud.util.Cache.getInstance(getProvider(), "Image.isSubscribed", Boolean.class, CacheLevel.REGION_ACCOUNT); final Iterable<Boolean> cachedIsSubscribed = cache.get(getContext()); if (cachedIsSubscribed != null && cachedIsSubscribed.iterator().hasNext()) { final Boolean isSubscribed = cachedIsSubscribed.iterator().next(); if (isSubscribed != null) { return isSubscribed; } } JoyentMethod method = new JoyentMethod(getProvider()); try { method.doGetJson(getProvider().getEndpoint(), "images"); } catch (JoyentException e) { if (e.getErrorType().equals(CloudErrorType.AUTHENTICATION)) { cache.put(getContext(), Collections.singleton(false)); return false; } throw e; } cache.put(getContext(), Collections.singleton(true)); return true; } @Override public @Nonnull Iterable<MachineImage> listImages( @Nullable ImageFilterOptions options ) throws CloudException, InternalException { APITrace.begin(getProvider(), "Image.listImages"); try { JoyentMethod method = new JoyentMethod(getProvider()); try { JSONArray arr = new JSONArray(method.doGetJson(getProvider().getEndpoint(), "images?public=false")); List<MachineImage> images = new ArrayList<MachineImage>(); for( int i = 0; i < arr.length(); i++ ) { JSONObject ob = arr.getJSONObject(i); MachineImage image = toMachineImage(ob); if( image != null && options.matches(image) ) { images.add(image); } } return images; } catch( JSONException e ) { throw new CloudException(e); } } finally { APITrace.end(); } } @Override public @Nonnull Iterable<String> listShares( @Nonnull String forMachineImageId ) throws CloudException, InternalException { // Joyent 7.1 doesn't support image sharing return Collections.emptyList(); } @Override public @Nonnull String[] mapServiceAction( @Nonnull ServiceAction action ) { return new String[0]; } @Override public void remove( @Nonnull String providerImageId, boolean checkState ) throws CloudException, InternalException { APITrace.begin(getProvider(), "Image.remove"); try { JoyentMethod method = new JoyentMethod(getProvider()); method.doDelete(getProvider().getEndpoint(), "images/" + providerImageId); } finally { APITrace.end(); } } @Override public @Nonnull Iterable<MachineImage> searchPublicImages( @Nonnull ImageFilterOptions options ) throws CloudException, InternalException { JoyentMethod method = new JoyentMethod(getProvider()); try { JSONArray arr = new JSONArray(method.doGetJson(getProvider().getEndpoint(), "images?public=true")); List<MachineImage> images = new ArrayList<MachineImage>(); for( int i = 0; i < arr.length(); i++ ) { MachineImage image = toMachineImage(arr.getJSONObject(i)); if( image != null && options.matches(image) ) { image.sharedWithPublic();// mark it as public regardless, since it is images.add(image); } } return images; } catch( JSONException e ) { throw new CloudException(e); } } private @Nullable MachineImage toMachineImage( @Nullable JSONObject json ) throws CloudException, InternalException { if( json == null ) { return null; } String regionId = getContext().getRegionId(); if( regionId == null ) { throw new InternalException("No region ID was specified for this request"); } String imageId, name, description, version, owner = OWNER_JOYENT; Architecture architecture = Architecture.I64; Platform platform = Platform.UNKNOWN; long created = 0L; Boolean isPublic = null; long minRamSize = 0L; MachineImageState state = MachineImageState.PENDING; try { if( json.has("id") ) { imageId = json.getString("id"); } else { return null; } if( json.has("name") ) { name = json.getString("name"); } else { name = imageId; } if( json.has("description") ) { description = json.getString("description"); } else { description = name + " (" + platform + ") [#" + imageId + "]"; } if( json.has("os") ) { String os = ( name == null ? json.getString("os") : name + " " + json.getString("os") ); platform = Platform.guess(os); architecture = guessArch(os); } if( json.has("created") ) { created = getProvider().parseTimestamp(json.getString("created")); } if( json.has("version") ) { version = json.getString("version"); name = name + ":" + version; } if( json.has("public") ) { isPublic = json.getBoolean("public"); owner = isPublic ? OWNER_JOYENT : getContext().getAccountNumber(); } if( json.has("requirements") ) { JSONObject requirements = json.getJSONObject("requirements"); if( requirements.has("min_ram") ) { minRamSize = requirements.getLong("min_ram"); } else if( requirements.has("min_memory") ) { minRamSize = requirements.getLong("min_memory"); } } if( json.has("state") ) { if( "active".equalsIgnoreCase(json.getString("state")) ) { state = MachineImageState.ACTIVE; } } } catch( JSONException e ) { throw new CloudException(e); } //old version only supported public images and did not return owner attribute final MachineImage machineImage = MachineImage.getInstance( owner, regionId, imageId, ImageClass.MACHINE, state, name, description, architecture, platform ).createdAt(created); if (isPublic != null) { machineImage.setTag("public", String.valueOf(isPublic)); if( isPublic ) { machineImage.sharedWithPublic(); } } if( minRamSize > 0L ) { machineImage.getProviderMetadata().put("min_ram", String.valueOf(minRamSize)); } return machineImage; } private static boolean surroundedByDigits(String hay, String needle ) throws Exception { hay = " " + hay + " "; int index = hay.indexOf(needle); if( index > 0 ) { if( Character.isDigit(hay.charAt(index-1)) && Character.isDigit(hay.charAt(index+needle.length())) ) return true; else { return false; } } throw new Exception(); // not found the needle } private static Architecture guessArch(String testString) { String[] needles = {"32", "386"}; for( String needle : needles ) { try { if( !surroundedByDigits(testString, needle) ) { return Architecture.I32; } } catch(Exception e) {} } return Architecture.I64; } }