1 Ext
.define('apt-repolist', {
2 extend
: 'Ext.data.Model',
18 Ext
.define('Proxmox.window.APTRepositoryAdd', {
19 extend
: 'Proxmox.window.Edit',
20 alias
: 'widget.pmxAPTRepositoryAdd',
25 subject
: gettext('Repository'),
28 initComponent: function() {
31 if (!me
.repoInfo
|| me
.repoInfo
.length
=== 0) {
32 throw "repository information not initialized";
35 let description
= Ext
.create('Ext.form.field.Display', {
36 fieldLabel
: gettext('Description'),
40 let status
= Ext
.create('Ext.form.field.Display', {
41 fieldLabel
: gettext('Status'),
43 renderer: function(value
) {
44 let statusText
= gettext('Not yet configured');
46 statusText
= Ext
.String
.format(
48 gettext('Configured'),
49 value
? gettext('enabled') : gettext('disabled'),
57 let repoSelector
= Ext
.create('Proxmox.form.KVComboBox', {
58 fieldLabel
: gettext('Repository'),
59 xtype
: 'proxmoxKVComboBox',
62 comboItems
: me
.repoInfo
.map(info
=> [info
.handle
, info
.name
]),
63 validator: function(renderedValue
) {
64 let handle
= this.value
;
65 // we cannot use this.callParent in instantiations
66 let valid
= Proxmox
.form
.KVComboBox
.prototype.validator
.call(this, renderedValue
);
68 if (!valid
|| !handle
) {
72 const info
= me
.repoInfo
.find(elem
=> elem
.handle
=== handle
);
78 return Ext
.String
.format(gettext('{0} is already configured'), renderedValue
);
83 change: function(f
, value
) {
84 const info
= me
.repoInfo
.find(elem
=> elem
.handle
=== value
);
85 description
.setValue(info
.description
);
86 status
.setValue(info
.status
);
91 repoSelector
.setValue(me
.repoInfo
[0].handle
);
99 repoSelector
: repoSelector
,
106 Ext
.define('Proxmox.node.APTRepositoriesErrors', {
107 extend
: 'Ext.grid.GridPanel',
109 xtype
: 'proxmoxNodeAPTRepositoriesErrors',
117 getRowClass
: (record
) => {
118 switch (record
.data
.status
) {
119 case 'warning': return 'proxmox-warning-row';
120 case 'critical': return 'proxmox-invalid-row';
131 renderer
: (value
) => `<i class="fa fa-fw ${Proxmox.Utils.get_health_icon(value, true)}"></i>`,
135 dataIndex
: 'message',
141 Ext
.define('Proxmox.node.APTRepositoriesGrid', {
142 extend
: 'Ext.grid.GridPanel',
143 xtype
: 'proxmoxNodeAPTRepositoriesGrid',
144 mixins
: ['Proxmox.Mixin.CBind'],
146 title
: gettext('APT Repositories'),
148 cls
: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
154 text
: gettext('Reload'),
155 iconCls
: 'fa fa-refresh',
156 handler: function() {
158 me
.up('proxmoxNodeAPTRepositories').reload();
162 text
: gettext('Add'),
167 onlineHelp
: '{onlineHelp}',
169 handler: function(button
, event
, record
) {
170 Proxmox
.Utils
.checked_command(() => {
172 let panel
= me
.up('proxmoxNodeAPTRepositories');
174 let extraParams
= {};
175 if (panel
.digest
!== undefined) {
176 extraParams
.digest
= panel
.digest
;
179 Ext
.create('Proxmox.window.APTRepositoryAdd', {
180 repoInfo
: me
.repoInfo
,
181 url
: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
183 extraRequestParams
: extraParams
,
184 onlineHelp
: me
.onlineHelp
,
186 destroy: function() {
196 xtype
: 'proxmoxAltTextButton',
197 defaultText
: gettext('Enable'),
198 altText
: gettext('Disable'),
202 text
: '{enableButtonText}',
204 handler: function(button
, event
, record
) {
206 let panel
= me
.up('proxmoxNodeAPTRepositories');
209 path
: record
.data
.Path
,
210 index
: record
.data
.Index
,
211 enabled
: record
.data
.Enabled
? 0 : 1, // invert
214 if (panel
.digest
!== undefined) {
215 params
.digest
= panel
.digest
;
218 Proxmox
.Utils
.API2Request({
219 url
: `/nodes/${panel.nodename}/apt/repositories`,
222 failure: function(response
, opts
) {
223 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
226 success: function(response
, opts
) {
234 sortableColumns
: false,
237 getRowClass
: (record
, index
) => record
.get('Enabled') ? '' : 'proxmox-disabled-row',
242 header
: gettext('Enabled'),
243 dataIndex
: 'Enabled',
245 renderer
: Proxmox
.Utils
.renderEnabledIcon
,
249 header
: gettext('Types'),
251 renderer: function(types
, cell
, record
) {
252 return types
.join(' ');
257 header
: gettext('URIs'),
259 renderer: function(uris
, cell
, record
) {
260 return uris
.join(' ');
265 header
: gettext('Suites'),
267 renderer: function(suites
, metaData
, record
) {
269 if (record
.data
.warnings
&& record
.data
.warnings
.length
> 0) {
270 let txt
= [gettext('Warning')];
271 record
.data
.warnings
.forEach((warning
) => {
272 if (warning
.property
=== 'Suites') {
273 txt
.push(warning
.message
);
276 metaData
.tdAttr
= `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
277 if (record
.data
.Enabled
) {
278 metaData
.tdCls
= 'proxmox-invalid-row';
279 err
= '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
281 metaData
.tdCls
= 'proxmox-warning-row';
282 err
= '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
285 return suites
.join(' ') + err
;
290 header
: gettext('Components'),
291 dataIndex
: 'Components',
292 renderer: function(components
, metaData
, record
) {
293 if (components
=== undefined) {
297 if (components
.length
=== 1) {
298 // FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
299 // like production-ready = <yes|no|other> (Option<bool>)
300 if (components
[0].match(/\w+(-no-subscription|test)\s*$/i)) {
301 metaData
.tdCls
= 'proxmox-warning-row';
302 err
= '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
304 let qtip
= components
[0].match(/no-subscription/)
305 ? gettext('The no-subscription repository is NOT production-ready')
306 : gettext('The test repository may contain unstable updates')
308 metaData
.tdAttr
= `data-qtip="${Ext.htmlEncode(qtip)}"`;
311 return components
.join(' ') + err
;
316 header
: gettext('Options'),
317 dataIndex
: 'Options',
318 renderer: function(options
, cell
, record
) {
323 let filetype
= record
.data
.FileType
;
326 options
.forEach(function(option
) {
327 let key
= option
.Key
;
328 if (filetype
=== 'list') {
329 let values
= option
.Values
.join(',');
330 text
+= `${key}=${values} `;
331 } else if (filetype
=== 'sources') {
332 let values
= option
.Values
.join(' ');
333 text
+= `${key}: ${values}<br>`;
335 throw "unknown file type";
343 header
: gettext('Origin'),
346 renderer: function(value
, meta
, rec
) {
347 if (typeof value
!== 'string' || value
.length
=== 0) {
348 value
= gettext('Other');
350 let cls
= 'fa fa-fw fa-question-circle-o';
351 let originType
= this.up('proxmoxNodeAPTRepositories').classifyOrigin(value
);
352 if (originType
=== 'Proxmox') {
353 cls
= 'pmx-itype-icon pmx-itype-icon-proxmox-x';
354 } else if (originType
=== 'Debian') {
355 cls
= 'pmx-itype-icon pmx-itype-icon-debian-swirl';
357 return `<i class='${cls}'></i> ${value}`;
361 header
: gettext('Comment'),
362 dataIndex
: 'Comment',
364 renderer
: Ext
.String
.htmlEncode
,
371 groupHeaderTpl
: '{[ "File: " + values.name ]} ({rows.length} repositor{[values.rows.length > 1 ? "ies" : "y"]})',
372 enableGroupingMenu
: false,
377 model
: 'apt-repolist',
387 initComponent: function() {
391 throw "no node name specified";
398 Ext
.define('Proxmox.node.APTRepositories', {
399 extend
: 'Ext.panel.Panel',
400 xtype
: 'proxmoxNodeAPTRepositories',
401 mixins
: ['Proxmox.Mixin.CBind'],
405 onlineHelp
: undefined,
407 product
: 'Proxmox VE', // default
409 classifyOrigin: function(origin
) {
411 if (origin
.match(/^\s*Proxmox\s*$/i)) {
413 } else if (origin
.match(/^\s*Debian\s*(:?Backports)?$/i)) {
420 xclass
: 'Ext.app.ViewController',
422 selectionChange: function(grid
, selection
) {
424 if (!selection
|| selection
.length
< 1) {
427 let rec
= selection
[0];
428 let vm
= me
.getViewModel();
429 vm
.set('selectionenabled', rec
.get('Enabled'));
433 updateState: function() {
435 let vm
= me
.getViewModel();
437 let store
= vm
.get('errorstore');
440 let status
= 'good'; // start with best, the helper below will downgrade if needed
441 let text
= gettext('All OK, you have production-ready repositories configured!');
443 let addGood
= message
=> store
.add({ status
: 'good', message
});
444 let addWarn
= (message
, important
) => {
445 if (status
!== 'critical') {
447 text
= important
? message
: gettext('Warning');
449 store
.add({ status
: 'warning', message
});
451 let addCritical
= (message
, important
) => {
453 text
= important
? message
: gettext('Error');
454 store
.add({ status
: 'critical', message
});
457 let errors
= vm
.get('errors');
458 errors
.forEach(error
=> addCritical(`${error.path} - ${error.error}`));
460 let activeSubscription
= vm
.get('subscriptionActive');
461 let enterprise
= vm
.get('enterpriseRepo');
462 let nosubscription
= vm
.get('noSubscriptionRepo');
463 let test
= vm
.get('testRepo');
465 enterprise
: vm
.get('cephEnterpriseRepo'),
466 nosubscription
: vm
.get('cephNoSubscriptionRepo'),
467 test
: vm
.get('cephTestRepo'),
469 let wrongSuites
= vm
.get('suitesWarning');
470 let mixedSuites
= vm
.get('mixedSuites');
472 if (!enterprise
&& !nosubscription
&& !test
) {
474 Ext
.String
.format(gettext('No {0} repository is enabled, you do not get any updates!'), vm
.get('product')),
476 } else if (errors
.length
> 0) {
477 // nothing extra, just avoid that we show "get updates"
478 } else if (enterprise
&& !nosubscription
&& !test
&& activeSubscription
) {
479 addGood(Ext
.String
.format(gettext('You get supported updates for {0}'), vm
.get('product')));
480 } else if (nosubscription
|| test
) {
481 addGood(Ext
.String
.format(gettext('You get updates for {0}'), vm
.get('product')));
485 addWarn(gettext('Some suites are misconfigured'));
489 addWarn(gettext('Detected mixed suites before upgrade'));
492 let productionReadyCheck
= (repos
, type
, noSubAlternateName
) => {
493 if (!activeSubscription
&& repos
.enterprise
) {
494 addWarn(Ext
.String
.format(
495 gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
500 if (repos
.nosubscription
) {
501 addWarn(Ext
.String
.format(
502 gettext('The {0}no-subscription{1} repository is not recommended for production use!'),
509 addWarn(Ext
.String
.format(
510 gettext('The {0}test repository may pull in unstable updates and is not recommended for production use!'),
516 productionReadyCheck({ enterprise
, nosubscription
, test
}, '', '');
517 // TODO drop alternate 'main' name when no longer relevant
518 productionReadyCheck(cephRepos
, 'Ceph ', '/main');
520 if (errors
.length
> 0) {
521 text
= gettext('Fatal parsing error for at least one repository');
524 let iconCls
= Proxmox
.Utils
.get_health_icon(status
, true);
535 product
: 'Proxmox VE', // default
537 suitesWarning
: false,
538 mixedSuites
: false, // used before major upgrade
539 subscriptionActive
: '',
540 noSubscriptionRepo
: '',
543 cephEnterpriseRepo
: '',
544 cephNoSubscriptionRepo
: '',
546 selectionenabled
: false,
550 enableButtonText
: (get) => get('selectionenabled')
551 ? gettext('Disable') : gettext('Enable'),
555 fields
: ['status', 'message'],
575 title
: gettext('Status'),
582 iconCls
: Proxmox
.Utils
.get_health_icon(undefined, true),
589 '<center class="centered-flex-column" style="font-size:15px;line-height: 25px;">',
590 '<i class="fa fa-4x {iconCls}"></i>',
596 xtype
: 'proxmoxNodeAPTRepositoriesErrors',
597 name
: 'repositoriesErrors',
601 store
: '{errorstore}',
607 xtype
: 'proxmoxNodeAPTRepositoriesGrid',
608 name
: 'repositoriesGrid',
611 nodename
: '{nodename}',
612 onlineHelp
: '{onlineHelp}',
614 majorUpgradeAllowed
: false, // TODO get release information from an API call?
616 selectionchange
: 'selectionChange',
621 check_subscription: function() {
623 let vm
= me
.getViewModel();
625 Proxmox
.Utils
.API2Request({
626 url
: `/nodes/${me.nodename}/subscription`,
628 failure
: (response
, opts
) => Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
),
629 success: function(response
, opts
) {
630 const res
= response
.result
;
631 const subscription
= !(!res
|| !res
.data
|| res
.data
.status
.toLowerCase() !== 'active');
632 vm
.set('subscriptionActive', subscription
);
633 me
.getController().updateState();
638 updateStandardRepos: function(standardRepos
) {
640 let vm
= me
.getViewModel();
642 let addButton
= me
.down('button[name=addRepo]');
644 addButton
.repoInfo
= [];
645 for (const standardRepo
of standardRepos
) {
646 const handle
= standardRepo
.handle
;
647 const status
= standardRepo
.status
;
649 if (handle
=== "enterprise") {
650 vm
.set('enterpriseRepo', status
);
651 } else if (handle
=== "no-subscription") {
652 vm
.set('noSubscriptionRepo', status
);
653 } else if (handle
=== 'test') {
654 vm
.set('testRepo', status
);
655 } else if (handle
.match(/^ceph-[a-zA-Z]+-enterprise$/)) {
656 vm
.set('cephEnterpriseRepo', status
);
657 } else if (handle
.match(/^ceph-[a-zA-Z]+-no-subscription$/)) {
658 vm
.set('cephNoSubscriptionRepo', status
);
659 } else if (handle
.match(/^ceph-[a-zA-Z]+-test$/)) {
660 vm
.set('cephTestRepo', status
);
662 me
.getController().updateState();
664 addButton
.repoInfo
.push(standardRepo
);
665 addButton
.digest
= me
.digest
;
668 addButton
.setDisabled(false);
673 let vm
= me
.getViewModel();
674 let repoGrid
= me
.down('proxmoxNodeAPTRepositoriesGrid');
676 me
.store
.load(function(records
, operation
, success
) {
680 let suitesWarning
= false;
682 // Usually different suites will give errors anyways, but before a major upgrade the
683 // current and the next suite are allowed, so it makes sense to check for mixed suites.
684 let checkMixedSuites
= false;
685 let mixedSuites
= false;
687 if (success
&& records
.length
> 0) {
688 let data
= records
[0].data
;
689 let files
= data
.files
;
690 errors
= data
.errors
;
691 digest
= data
.digest
;
694 for (const info
of data
.infos
) {
695 let path
= info
.path
;
696 let idx
= info
.index
;
701 if (!infos
[path
][idx
]) {
705 // Used as a heuristic to detect mixed repositories pre-upgrade. The
706 // warning is set on all repositories that do configure the next suite.
707 gotIgnorePreUpgradeWarning
: false,
711 if (info
.kind
=== 'origin') {
712 infos
[path
][idx
].origin
= info
.message
;
713 } else if (info
.kind
=== 'warning') {
714 infos
[path
][idx
].warnings
.push(info
);
715 } else if (info
.kind
=== 'ignore-pre-upgrade-warning') {
716 infos
[path
][idx
].gotIgnorePreUpgradeWarning
= true;
717 if (!repoGrid
.majorUpgradeAllowed
) {
718 infos
[path
][idx
].warnings
.push(info
);
720 checkMixedSuites
= true;
726 files
.forEach(function(file
) {
727 for (let n
= 0; n
< file
.repositories
.length
; n
++) {
728 let repo
= file
.repositories
[n
];
729 repo
.Path
= file
.path
;
731 if (infos
[file
.path
] && infos
[file
.path
][n
]) {
732 repo
.Origin
= infos
[file
.path
][n
].origin
|| Proxmox
.Utils
.unknownText
;
733 repo
.warnings
= infos
[file
.path
][n
].warnings
|| [];
736 if (repo
.warnings
.some(w
=> w
.property
=== 'Suites')) {
737 suitesWarning
= true;
740 let originType
= me
.classifyOrigin(repo
.Origin
);
741 // Only Proxmox and Debian repositories checked here, because the
742 // warning can be missing for others for a different reason (e.g.
743 // using 'stable' or non-Debian code names).
744 if (checkMixedSuites
&& repo
.Types
.includes('deb') &&
745 (originType
=== 'Proxmox' || originType
=== 'Debian') &&
746 !infos
[file
.path
][n
].gotIgnorePreUpgradeWarning
756 repoGrid
.store
.loadData(gridData
);
758 me
.updateStandardRepos(data
['standard-repos']);
763 vm
.set('errors', errors
);
764 vm
.set('suitesWarning', suitesWarning
);
765 vm
.set('mixedSuites', mixedSuites
);
766 me
.getController().updateState();
769 me
.check_subscription();
773 activate: function() {
779 initComponent: function() {
783 throw "no node name specified";
786 let store
= Ext
.create('Ext.data.Store', {
789 url
: `/api2/json/nodes/${me.nodename}/apt/repositories`,
793 Ext
.apply(me
, { store
: store
});
795 Proxmox
.Utils
.monStoreErrors(me
, me
.store
, true);
799 me
.getViewModel().set('product', me
.product
);