diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs b/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs index c026f36a2..a8ed5c5f7 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackConfig.cs @@ -19,10 +19,13 @@ using System; using Duplicati.Library.Interface; using System.Collections.Generi 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 { Providers, } + { 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 { Providers, + Versions, } public SwiftConfig() { - } public System.Collections.Generic.IDictionary Execute(System.Collections.Generic.IDictionary options) { string k; options.TryGetValue(KEY_CONFIGTYPE, out k); if (string.IsNullOrWhiteSpace(k)) k = DEFAULT_CONFIG_TYPE_STR; ConfigType ct; if (!Enum.TryParse(k, true, out ct)) ct = DEFAULT_CONFIG_TYPE; switch (ct) { default: return OpenStack.OpenStackStorage.KNOWN_OPENSTACK_PROVIDERS.ToDictionary((x) => x.Key, (y) => y.Value); } } public string Key { get { return "openstack-getconfig"; } } public string DisplayName { get { return "OpenStack configuration module"; } } public string Description { get { return "Exposes OpenStack configuration as a web module"; } } public System.Collections.Generic.IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, "The config to get", "Provides different config values", DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) }); } } + } public System.Collections.Generic.IDictionary Execute(System.Collections.Generic.IDictionary options) { string k; options.TryGetValue(KEY_CONFIGTYPE, out k); if (string.IsNullOrWhiteSpace(k)) k = DEFAULT_CONFIG_TYPE_STR; ConfigType ct; if (!Enum.TryParse(k, true, out ct)) ct = DEFAULT_CONFIG_TYPE; switch (ct) { + case ConfigType.Versions: + return OpenStack.OpenStackStorage.OPENSTACK_VERSIONS.ToDictionary((x) => x.Key, (y) => y.Value); default: return OpenStack.OpenStackStorage.KNOWN_OPENSTACK_PROVIDERS.ToDictionary((x) => x.Key, (y) => y.Value); } } public string Key { get { return "openstack-getconfig"; } } public string DisplayName { get { return "OpenStack configuration module"; } } public string Description { get { return "Exposes OpenStack configuration as a web module"; } } public System.Collections.Generic.IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(KEY_CONFIGTYPE, CommandLineArgument.ArgumentType.Enumeration, "The config to get", "Provides different config values", DEFAULT_CONFIG_TYPE_STR, Enum.GetNames(typeof(ConfigType))) }); } } } } diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index a494ad2b3..7d94cba02 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -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[] KNOWN_OPENSTACK_PROVIDERS = new KeyValuePair[] { + public static readonly KeyValuePair[] KNOWN_OPENSTACK_PROVIDERS = new KeyValuePair[] { new KeyValuePair("Rackspace US", "https://identity.api.rackspacecloud.com/v2.0"), new KeyValuePair("Rackspace UK", "https://lon.identity.api.rackspacecloud.com/v2.0"), new KeyValuePair("OVH Cloud Storage", "https://auth.cloud.ovh.net/v2.0"), new KeyValuePair("Selectel Cloud Storage", "https://auth.selcdn.ru"), }; - + + public static readonly KeyValuePair[] OPENSTACK_VERSIONS = new KeyValuePair[] { + new KeyValuePair("v2.0", "v2"), + new KeyValuePair("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( + 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(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), }); } diff --git a/Duplicati/Library/Backend/OpenStack/Strings.cs b/Duplicati/Library/Backend/OpenStack/Strings.cs index 74faa2ad5..692083b5f 100644 --- a/Duplicati/Library/Backend/OpenStack/Strings.cs +++ b/Duplicati/Library/Backend/OpenStack/Strings.cs @@ -17,5 +17,11 @@ 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"); } } public static string UsernameOptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } } public static string UsernameOptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } } public static string TenantnameOptionLong { get { return LC.L(@"The Tenant Name is commonly the paying user account name. This option must be supplied when authenticating with a password, but is not required when using an API key."); } } public static string TenantnameOptionShort { get { return LC.L(@"Supplies the Tenant Name used to connect to the server"); } } public static string ApikeyOptionLong { get { return LC.L(@"The API key can be used to connect without supplying a password and tenant ID with some providers."); } } public static string ApikeyOptionShort { get { return LC.L(@"Supplies the API key used to connect to the server"); } } public static string AuthuriOptionLong(string providers) { return LC.L(@"The authentication URL is used to authenticate the user and find the storage service. The URL commonly ends with ""/v2.0"". Known providers are: {0}{1}", System.Environment.NewLine, providers); } public static string AuthuriOptionShort { get { return LC.L(@"Supplies the authentication URL"); } } public static string RegionOptionLong { get { return LC.L(@"This option is only used when creating a container, and is used to indicate where the container should be placed. Consult your provider for a list of valid regions, or leave empty for the default region."); } } public static string RegionOptionShort { get { return LC.L(@"Supplies the region used for creating a container"); } } } } + 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"); } } public static string DomainnameOptionLong { get { return LC.L(@"The domain name of the user used to connect to the server."); } } + public static string DomainnameOptionShort { get { return LC.L(@"Supplies the domain used to connect to the server"); } } + public static string UsernameOptionLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } } + public static string UsernameOptionShort { get { return LC.L(@"Supplies the username used to connect to the server"); } } + public static string TenantnameOptionLong { get { return LC.L(@"The Tenant Name is commonly the paying user account name. This option must be supplied when authenticating with a password, but is not required when using an API key."); } } public static string TenantnameOptionShort { get { return LC.L(@"Supplies the Tenant Name used to connect to the server"); } } public static string ApikeyOptionLong { get { return LC.L(@"The API key can be used to connect without supplying a password and tenant ID with some providers."); } } public static string ApikeyOptionShort { get { return LC.L(@"Supplies the API key used to connect to the server"); } } public static string AuthuriOptionLong(string providers) { return LC.L(@"The authentication URL is used to authenticate the user and find the storage service. The URL commonly ends with ""/v2.0"". Known providers are: {0}{1}", System.Environment.NewLine, providers); } public static string AuthuriOptionShort { get { return LC.L(@"Supplies the authentication URL"); } } public static string VersionOptionLong { get { return LC.L(@"The keystone API version to use, valid values are 'v2' and 'v3'."); } } + public static string VersionOptionShort { get { return LC.L(@"The keystone API version to use"); } } + public static string RegionOptionLong { get { return LC.L(@"This option is only used when creating a container, and is used to indicate where the container should be placed. Consult your provider for a list of valid regions, or leave empty for the default region."); } } public static string RegionOptionShort { get { return LC.L(@"Supplies the region used for creating a container"); } } } } diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js index 668509b1c..a9ff0804b 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js @@ -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')) && diff --git a/Duplicati/Server/webroot/ngax/templates/backends/openstack.html b/Duplicati/Server/webroot/ngax/templates/backends/openstack.html index 8768578d8..52e5c4dd9 100644 --- a/Duplicati/Server/webroot/ngax/templates/backends/openstack.html +++ b/Duplicati/Server/webroot/ngax/templates/backends/openstack.html @@ -14,6 +14,18 @@ +
+ + + + +
+ +
+ + +