Merge pull request #3154 from epol/master
Openstack keystone v3 support
This commit is contained in:
@@ -19,10 +19,13 @@ using System;
using Duplicati.Library.Interface;
using System.Collections.Generi
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Library.Backend.OpenStack
|
||||
namespace Duplicati.Library.Backend.OpenStack
|
||||
{
|
||||
public class SwiftConfig : IWebModule
|
||||
{
|
||||
{
|
||||
private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.Providers;
|
||||
private static readonly string DEFAULT_CONFIG_TYPE_STR = Enum.GetName(typeof(ConfigType), DEFAULT_CONFIG_TYPE);
|
||||
private const string KEY_CONFIGTYPE = "openstack-config";
|
||||
|
||||
public enum ConfigType
|
||||
{
|
||||
|
||||
@@ -18,8 +18,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Interface;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Duplicati.Library.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Duplicati.Library.Strings;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
@@ -28,10 +30,12 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
{
|
||||
public class OpenStackStorage : IBackend, IStreamingBackend
|
||||
{
|
||||
private const string DOMAINNAME_OPTION = "openstack-domain-name";
|
||||
private const string USERNAME_OPTION = "auth-username";
|
||||
private const string PASSWORD_OPTION = "auth-password";
|
||||
private const string TENANTNAME_OPTION = "openstack-tenant-name";
|
||||
private const string AUTHURI_OPTION = "openstack-authuri";
|
||||
private const string VERSION_OPTION = "openstack-version";
|
||||
private const string APIKEY_OPTION = "openstack-apikey";
|
||||
private const string REGION_OPTION = "openstack-region";
|
||||
|
||||
@@ -41,9 +45,11 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
private string m_container;
|
||||
private string m_prefix;
|
||||
|
||||
private readonly string m_domainName;
|
||||
private string m_username;
|
||||
private string m_password;
|
||||
private string m_authUri;
|
||||
private readonly string m_version;
|
||||
private string m_tenantName;
|
||||
private string m_apikey;
|
||||
private string m_region;
|
||||
@@ -53,13 +59,110 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
private WebHelper m_helper = null;
|
||||
private OpenStackAuthResponse.TokenClass m_accessToken;
|
||||
|
||||
public static readonly KeyValuePair<string, string>[] KNOWN_OPENSTACK_PROVIDERS = new KeyValuePair<string,string>[] {
|
||||
public static readonly KeyValuePair<string, string>[] KNOWN_OPENSTACK_PROVIDERS = new KeyValuePair<string, string>[] {
|
||||
new KeyValuePair<string, string>("Rackspace US", "https://identity.api.rackspacecloud.com/v2.0"),
|
||||
new KeyValuePair<string, string>("Rackspace UK", "https://lon.identity.api.rackspacecloud.com/v2.0"),
|
||||
new KeyValuePair<string, string>("OVH Cloud Storage", "https://auth.cloud.ovh.net/v2.0"),
|
||||
new KeyValuePair<string, string>("Selectel Cloud Storage", "https://auth.selcdn.ru"),
|
||||
};
|
||||
|
||||
|
||||
public static readonly KeyValuePair<string, string>[] OPENSTACK_VERSIONS = new KeyValuePair<string, string>[] {
|
||||
new KeyValuePair<string, string>("v2.0", "v2"),
|
||||
new KeyValuePair<string, string>("v3", "v3"),
|
||||
};
|
||||
|
||||
|
||||
private class Keystone3AuthRequest
|
||||
{
|
||||
public class AuthContainer
|
||||
{
|
||||
public Identity identity { get; set; }
|
||||
public Scope scope { get; set; }
|
||||
}
|
||||
|
||||
public class Identity
|
||||
{
|
||||
[JsonProperty(ItemConverterType=typeof(StringEnumConverter))]
|
||||
public IdentityMethods[] methods { get; set; }
|
||||
|
||||
[JsonProperty("password", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public PasswordBasedRequest PasswordCredentials { get; set; }
|
||||
|
||||
public Identity()
|
||||
{
|
||||
this.methods = new [] { IdentityMethods.password };
|
||||
}
|
||||
}
|
||||
|
||||
public class Scope
|
||||
{
|
||||
public Project project;
|
||||
}
|
||||
|
||||
public enum IdentityMethods
|
||||
{
|
||||
password,
|
||||
}
|
||||
|
||||
public class PasswordBasedRequest
|
||||
{
|
||||
public UserCredentials user { get; set; }
|
||||
}
|
||||
|
||||
public class UserCredentials
|
||||
{
|
||||
public Domain domain { get; set; }
|
||||
public string name { get; set; }
|
||||
public string password { get; set; }
|
||||
|
||||
public UserCredentials()
|
||||
{
|
||||
}
|
||||
public UserCredentials(Domain domain, string name, string password)
|
||||
{
|
||||
this.domain = domain;
|
||||
this.name = name;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class Domain
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
public Domain(string name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public class Project {
|
||||
public Domain domain { get; set; }
|
||||
public string name { get; set; }
|
||||
|
||||
public Project(Domain domain, string name)
|
||||
{
|
||||
this.domain = domain;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public AuthContainer auth { get; set; }
|
||||
|
||||
public Keystone3AuthRequest(string domain_name, string username, string password, string project_name)
|
||||
{
|
||||
Domain domain = new Domain(domain_name);
|
||||
|
||||
this.auth = new AuthContainer();
|
||||
this.auth.identity = new Identity();
|
||||
this.auth.identity.PasswordCredentials = new PasswordBasedRequest();
|
||||
this.auth.identity.PasswordCredentials.user = new UserCredentials(domain,username,password);
|
||||
this.auth.scope = new Scope();
|
||||
this.auth.scope.project = new Project(domain, project_name);
|
||||
}
|
||||
}
|
||||
|
||||
private class OpenStackAuthRequest
|
||||
{
|
||||
public class AuthContainer
|
||||
@@ -106,14 +209,16 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
|
||||
if (string.IsNullOrEmpty(apikey))
|
||||
{
|
||||
this.auth.PasswordCredentials = new PasswordBasedRequest() {
|
||||
this.auth.PasswordCredentials = new PasswordBasedRequest()
|
||||
{
|
||||
username = username,
|
||||
password = password,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
this.auth.ApiCredentials = new ApiKeyBasedRequest() {
|
||||
this.auth.ApiCredentials = new ApiKeyBasedRequest()
|
||||
{
|
||||
username = username,
|
||||
apiKey = apikey
|
||||
};
|
||||
@@ -122,6 +227,32 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
}
|
||||
}
|
||||
|
||||
private class Keystone3AuthResponse
|
||||
{
|
||||
public TokenClass token { get; set; }
|
||||
|
||||
public class EndpointItem
|
||||
{
|
||||
// 'interface' is a reserved keyword, so we need this decorator to map it
|
||||
[JsonProperty(PropertyName = "interface")]
|
||||
public string interface_name { get; set; }
|
||||
public string region { get; set; }
|
||||
public string url { get; set; }
|
||||
}
|
||||
|
||||
public class CatalogItem
|
||||
{
|
||||
public EndpointItem[] endpoints { get; set; }
|
||||
public string name { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
public class TokenClass
|
||||
{
|
||||
public CatalogItem[] catalog { get; set; }
|
||||
public DateTime? expires_at { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
private class OpenStackAuthResponse
|
||||
{
|
||||
public AccessClass access { get; set; }
|
||||
@@ -195,10 +326,12 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
if (m_prefix.StartsWith("/", StringComparison.Ordinal))
|
||||
m_prefix = m_prefix.Substring(1);
|
||||
|
||||
options.TryGetValue(DOMAINNAME_OPTION, out m_domainName);
|
||||
options.TryGetValue(USERNAME_OPTION, out m_username);
|
||||
options.TryGetValue(PASSWORD_OPTION, out m_password);
|
||||
options.TryGetValue(TENANTNAME_OPTION, out m_tenantName);
|
||||
options.TryGetValue(AUTHURI_OPTION, out m_authUri);
|
||||
options.TryGetValue(VERSION_OPTION, out m_version);
|
||||
options.TryGetValue(APIKEY_OPTION, out m_apikey);
|
||||
options.TryGetValue(REGION_OPTION, out m_region);
|
||||
|
||||
@@ -207,12 +340,26 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
if (string.IsNullOrWhiteSpace(m_authUri))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(AUTHURI_OPTION), "OpenStackMissingAuthUri");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(m_apikey))
|
||||
switch (m_version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(m_password))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(PASSWORD_OPTION), "OpenStackMissingPassword");
|
||||
if (string.IsNullOrWhiteSpace(m_tenantName))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(TENANTNAME_OPTION), "OpenStackMissingTenantName");
|
||||
case "v3":
|
||||
if (string.IsNullOrWhiteSpace(m_password))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(PASSWORD_OPTION), "OpenStackMissingPassword");
|
||||
if (string.IsNullOrWhiteSpace(m_domainName))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(DOMAINNAME_OPTION), "OpenStackMissingDomainName");
|
||||
if (string.IsNullOrWhiteSpace(m_tenantName))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(TENANTNAME_OPTION), "OpenStackMissingTenantName");
|
||||
break;
|
||||
case "v2":
|
||||
default:
|
||||
if (string.IsNullOrWhiteSpace(m_apikey))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(m_password))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(PASSWORD_OPTION), "OpenStackMissingPassword");
|
||||
if (string.IsNullOrWhiteSpace(m_tenantName))
|
||||
throw new UserInformationException(Strings.OpenStack.MissingOptionError(TENANTNAME_OPTION), "OpenStackMissingTenantName");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
m_helper = new WebHelper(this);
|
||||
@@ -239,7 +386,65 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
return JoinUrls(JoinUrls(uri, fragment1), fragment2);
|
||||
}
|
||||
|
||||
private OpenStackAuthResponse GetAuthResponse()
|
||||
private void GetAuthResponse()
|
||||
{
|
||||
switch (this.m_version)
|
||||
{
|
||||
case "v3":
|
||||
GetKeystone3AuthResponse();
|
||||
break;
|
||||
case "v2":
|
||||
default:
|
||||
GetOpenstackAuthResponse();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Keystone3AuthResponse GetKeystone3AuthResponse()
|
||||
{
|
||||
var helper = new JSONWebHelper();
|
||||
|
||||
var req = helper.CreateRequest(JoinUrls(m_authUri, "auth/tokens"));
|
||||
req.Accept = "application/json";
|
||||
req.Method = "POST";
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(
|
||||
JsonConvert.SerializeObject(
|
||||
new Keystone3AuthRequest(m_domainName, m_username, m_password, m_tenantName)
|
||||
));
|
||||
|
||||
req.ContentLength = data.Length;
|
||||
req.ContentType = "application/json; charset=UTF-8";
|
||||
|
||||
using (var rs = req.GetRequestStream())
|
||||
rs.Write(data, 0, data.Length);
|
||||
|
||||
WebResponse http_response = req.GetResponse();
|
||||
|
||||
Keystone3AuthResponse resp;
|
||||
using (var reader = new StreamReader(http_response.GetResponseStream()))
|
||||
{
|
||||
resp = Newtonsoft.Json.JsonConvert.DeserializeObject<Keystone3AuthResponse>(
|
||||
reader.ReadToEnd());
|
||||
}
|
||||
|
||||
string token = http_response.Headers["X-Subject-Token"];
|
||||
this.m_accessToken = new OpenStackAuthResponse.TokenClass();
|
||||
this.m_accessToken.id = token;
|
||||
this.m_accessToken.expires = resp.token.expires_at;
|
||||
|
||||
// Grab the endpoint now that we have received it anyway
|
||||
var fileservice = resp.token.catalog.FirstOrDefault(x => string.Equals(x.type, "object-store", StringComparison.OrdinalIgnoreCase));
|
||||
if (fileservice == null)
|
||||
throw new Exception("No object-store service found, is this service supported by the provider?");
|
||||
|
||||
var endpoint = fileservice.endpoints.FirstOrDefault(x => (string.Equals(m_region, x.region) && string.Equals(x.interface_name, "public", StringComparison.OrdinalIgnoreCase))) ?? fileservice.endpoints.First();
|
||||
m_simplestorageendpoint = endpoint.url;
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
private OpenStackAuthResponse GetOpenstackAuthResponse()
|
||||
{
|
||||
var helper = new JSONWebHelper();
|
||||
|
||||
@@ -406,11 +611,13 @@ namespace Duplicati.Library.Backend.OpenStack
|
||||
authuris.AppendLine(string.Format("{0}: {1}", s.Key, s.Value));
|
||||
|
||||
return new List<ICommandLineArgument>(new CommandLineArgument[] {
|
||||
new CommandLineArgument(DOMAINNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.DomainnameOptionShort, Strings.OpenStack.UsernameOptionLong),
|
||||
new CommandLineArgument(USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.UsernameOptionShort, Strings.OpenStack.UsernameOptionLong),
|
||||
new CommandLineArgument(PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.PasswordOptionShort, Strings.OpenStack.PasswordOptionLong(TENANTNAME_OPTION)),
|
||||
new CommandLineArgument(TENANTNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.TenantnameOptionShort, Strings.OpenStack.TenantnameOptionLong),
|
||||
new CommandLineArgument(APIKEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.ApikeyOptionShort, Strings.OpenStack.ApikeyOptionLong),
|
||||
new CommandLineArgument(AUTHURI_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.AuthuriOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())),
|
||||
new CommandLineArgument(VERSION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.VersionOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())),
|
||||
new CommandLineArgument(REGION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.RegionOptionShort, Strings.OpenStack.RegionOptionLong),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,5 +17,11 @@
|
||||
|
||||
using Duplicati.Library.Localization.Short;
|
||||
|
||||
namespace Duplicati.Library.Backend.Strings
|
||||
namespace Duplicati.Library.Backend.Strings
|
||||
{
|
||||
internal static class OpenStack {
|
||||
public static string Description { get { return LC.L(@"This backend can read and write data to Swift (OpenStack Object Storage). Supported format is ""openstack://container/folder""."); } }
|
||||
public static string DisplayName { get { return LC.L(@"OpenStack Simple Storage"); } }
|
||||
public static string MissingOptionError(string optionname) { return LC.L(@"Missing required option: {0}", optionname); }
|
||||
public static string PasswordOptionLong(string tenantnameoption) { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD"". If the password is supplied, --{0} must also be set", tenantnameoption); }
|
||||
public static string PasswordOptionShort { get { return LC.L(@"Supplies the password used to connect to the server"); } }
|
||||
|
||||
@@ -215,6 +215,18 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
|
||||
if (scope.openstack_server == undefined && scope.openstack_server_custom == undefined)
|
||||
scope.openstack_server = 'https://identity.api.rackspacecloud.com/';
|
||||
}
|
||||
|
||||
if (scope.openstack_versions == null) {
|
||||
AppService.post('/webmodule/openstack-getconfig', {'openstack-config': 'Versions'}).then(function(data) {
|
||||
scope.openstack_versions = data.data.Result;
|
||||
if (scope.openstack_version == undefined )
|
||||
scope.openstack_version = 'v2.0';
|
||||
|
||||
}, AppUtils.connectionError);
|
||||
} else {
|
||||
if (scope.openstack_version == undefined )
|
||||
scope.openstack_version = 'v2.0';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -312,12 +324,14 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
|
||||
EditUriBackendConfig.parsers['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
|
||||
|
||||
EditUriBackendConfig.parsers['openstack'] = function(scope, module, server, port, path, options) {
|
||||
scope.openstack_domainname = options['--openstack-domain-name'];
|
||||
scope.openstack_server = scope.openstack_server_custom = options['--openstack-authuri'];
|
||||
scope.openstack_version = options['--openstack-version'];
|
||||
scope.openstack_tenantname = options['--openstack-tenant-name'];
|
||||
scope.openstack_apikey = options['--openstack-apikey'];
|
||||
scope.openstack_region = options['--openstack-region'];
|
||||
|
||||
var nukeopts = ['--openstack-authuri', '--openstack-tenant-name', '--openstack-apikey', '--openstack-region'];
|
||||
var nukeopts = ['--openstack-domain-name', '--openstack-authuri', '--openstack-tenant-name', '--openstack-apikey', '--openstack-region', '--openstack-version'];
|
||||
for(var x in nukeopts)
|
||||
delete options[nukeopts[x]];
|
||||
|
||||
@@ -464,18 +478,24 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
|
||||
|
||||
EditUriBackendConfig.builders['openstack'] = function(scope) {
|
||||
var opts = {
|
||||
'openstack-domain-name': scope.openstack_domainname,
|
||||
'openstack-authuri': AppUtils.contains_value(scope.openstack_providers, scope.openstack_server) ? scope.openstack_server : scope.openstack_server_custom,
|
||||
'openstack-version': scope.openstack_version,
|
||||
'openstack-tenant-name': scope.openstack_tenantname,
|
||||
'openstack-apikey': scope.openstack_apikey,
|
||||
'openstack-region': scope.openstack_region
|
||||
};
|
||||
|
||||
if ((opts['openstack-domain-name'] || '') == '')
|
||||
delete opts['openstack-domain-name'];
|
||||
if ((opts['openstack-tenant-name'] || '') == '')
|
||||
delete opts['openstack-tenant-name'];
|
||||
if ((opts['openstack-apikey'] || '') == '')
|
||||
delete opts['openstack-apikey'];
|
||||
if ((opts['openstack-region'] || '') == '')
|
||||
delete opts['openstack-region'];
|
||||
if ((opts['openstack-version'] || '') == '')
|
||||
delete opts['openstack-version'];
|
||||
|
||||
EditUriBackendConfig.merge_in_advanced_options(scope, opts);
|
||||
|
||||
@@ -763,29 +783,45 @@ backupApp.service('EditUriBuiltins', function(AppService, AppUtils, SystemInfo,
|
||||
|
||||
EditUriBackendConfig.validaters['openstack'] = function(scope, continuation) {
|
||||
var res =
|
||||
EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) &&
|
||||
EditUriBackendConfig.require_field(scope, 'Path', gettextCatalog.getString('Bucket Name'));
|
||||
|
||||
EditUriBackendConfig.require_field(scope, 'Username', gettextCatalog.getString('Username')) &&
|
||||
EditUriBackendConfig.require_field(scope, 'Path', gettextCatalog.getString('Bucket Name'));
|
||||
|
||||
if (res && (scope['openstack_server'] || '').trim().length == 0 && (scope['openstack_server_custom'] || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must select or fill in the AuthURI'));
|
||||
|
||||
if (((scope.openstack_version) || '').trim() == 'v3') {
|
||||
if (res && (scope.Password || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a password to use v3 API'));
|
||||
|
||||
if (res && ((scope.openstack_domainname) || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a domain name to use v3 API'));
|
||||
|
||||
if (res && ((scope.openstack_tenantname) || '').trim().length == 0)
|
||||
res = EditUriBackendCOnfig.show_error_dialog(gettextCatalog.getString('You must enter a tenant (aka project) name to use v3 API'));
|
||||
|
||||
if (((scope.openstack_apikey) || '').trim().length == 0) {
|
||||
|
||||
if (res && (scope.Password || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter either a password or an API Key'));
|
||||
|
||||
if (res && ((scope.openstack_tenantname) || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a tenant name if you do not provide an API Key'));
|
||||
|
||||
} else {
|
||||
if (res && (scope.Password || '').trim().length != 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter either a password or an API Key, not both'));
|
||||
}
|
||||
|
||||
if (res && (scope.openstack_apikey || '').trim().length != 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('Openstack API Key are not supported in v3 keystone API.'));
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
if (((scope.openstack_apikey) || '').trim().length == 0) {
|
||||
|
||||
if (res && (scope.Password || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter either a password or an API Key'));
|
||||
|
||||
if (res && ((scope.openstack_tenantname) || '').trim().length == 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a tenant name if you do not provide an API Key'));
|
||||
|
||||
} else {
|
||||
if (res && (scope.Password || '').trim().length != 0)
|
||||
res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter either a password or an API Key, not both'));
|
||||
}
|
||||
}
|
||||
if (res)
|
||||
continuation();
|
||||
};
|
||||
|
||||
|
||||
EditUriBackendConfig.validaters['s3'] = function(scope, continuation) {
|
||||
var res =
|
||||
EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Bucket Name')) &&
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="input text">
|
||||
<label for="openstack_version" translate>Keystone API version</label>
|
||||
|
||||
<select name="openstack_version" id="openstack_version" ng-model="$parent.openstack_version" ng-options="v as k + ' (' + v + ')' for (k, v) in openstack_versions | orderBy: k">
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="input text">
|
||||
<label for="openstack_domainname" translate>Domain Name</label>
|
||||
<input type="text" name="openstack_domainname" id="openstack_domainname" ng-model="$parent.openstack_domainname" placeholder="{{'User domain name' | translate}}" />
|
||||
</div>
|
||||
<div class="input text">
|
||||
<label for="openstack_username" translate>Username</label>
|
||||
<input type="text" name="openstack_username" id="openstack_username" ng-model="$parent.Username" placeholder="{{'Authentication username' | translate}}" />
|
||||
|
||||
Reference in New Issue
Block a user