Repository: Pyomo/pyomo

Issue:
Unallowed character (@) found in CPLEX solution file path/name
## Summary

On Linux, solving a model with CPLEX solver where the user has a name of the form `user@D01.arc.local` is not allowed.

### Steps to reproduce the issue

As outlined in Summary above

### Error Message

```console
Traceback (most recent call last):
  File "/trinity/home/user/proj/src/model_concrete.py", line 1603, in <module>
    RESULTS = SOLVER.solve(MODEL, tee=True, logfile="solve.log")
  File "/trinity/home/user/opt/miniconda3/envs/env/lib/python3.10/site-packages/pyomo/opt/base/solvers.py", line 570, in solve
    self._presolve(*args, **kwds)
  File "/trinity/home/user/opt/miniconda3/envs/env/lib/python3.10/site-packages/pyomo/solvers/plugins/solvers/CPLEX.py", line 342, in _presolve
    ILMLicensedSystemCallSolver._presolve(self, *args, **kwds)
  File "/trinity/home/user/opt/miniconda3/envs/env/lib/python3.10/site-packages/pyomo/opt/solver/shellcmd.py", line 221, in _presolve
    self._command = self.create_command_line(
  File "/trinity/home/user/opt/miniconda3/envs/env/lib/python3.10/site-packages/pyomo/solvers/plugins/solvers/CPLEX.py", line 417, in create_command_line
    self._soln_file = _validate_file_name(self, self._soln_file, "solution")
  File "/trinity/home/user/opt/miniconda3/envs/env/lib/python3.10/site-packages/pyomo/solvers/plugins/solvers/CPLEX.py", line 53, in _validate_file_name
    raise ValueError(
ValueError: Unallowed character (@) found in CPLEX solution file path/name.
        For portability reasons, only [a-zA-Z0-9 ~:.-_/] are allowed.
```

### Information on your system

Pyomo version: 6.4.2
Python version: 3.10.4
Operating system: x86_64 GNU/Linux
How Pyomo was installed (PyPI, conda, source): conda
Solver (if applicable): CPLEX


### Additional information

Manually adding "@" to the list of validation characters [here](https://github.com/Pyomo/pyomo/blob/3c50ee20e8d93f6803cbc960b194c0372b9fd207/pyomo/solvers/plugins/solvers/CPLEX.py#L69) makes the error go away.



Discussion:

Resolving patch:
diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py
--- a/pyomo/solvers/plugins/solvers/CPLEX.py
+++ b/pyomo/solvers/plugins/solvers/CPLEX.py
@@ -66,8 +66,10 @@ def _validate_file_name(cplex, filename, description):
                 "CPLEX or remove the space from the %s path."
                 % (description, filename, description))
     return filename
-_validate_file_name.allowed_characters = r"a-zA-Z0-9 ~:\.\-_\%s" % (
-    os.path.sep,)
+# The full list of allowed characters, per IBM, is:
+    # (a-z, A-Z, 0-9) or ! " # $ % & ( ) / , . ; ? @ _ ` ' { } | ~
+_validate_file_name.allowed_characters = \
+    r"a-zA-Z0-9 ~:;,!'`|\$\(\)\{\}\?\#\&\.\-_\@\%s" % (os.path.sep,)
 _validate_file_name.illegal_characters = re.compile(
     '[^%s]' % (_validate_file_name.allowed_characters,))
 


Test patch:
diff --git a/pyomo/solvers/tests/checks/test_cplex.py b/pyomo/solvers/tests/checks/test_cplex.py
--- a/pyomo/solvers/tests/checks/test_cplex.py
+++ b/pyomo/solvers/tests/checks/test_cplex.py
@@ -61,6 +61,33 @@ def test_validate_file_name(self):
         with self.assertRaisesRegex(ValueError, msg):
             _validate_file_name(_128, fname, 'xxx')
 
+        # check allowable characters
+        fname = 'foo$$bar.lp'
+        self.assertEqual(fname, _validate_file_name(_126, fname, 'xxx'))
+        self.assertEqual(fname, _validate_file_name(_128, fname, 'xxx'))
+        fname = 'foo_bar.lp'
+        self.assertEqual(fname, _validate_file_name(_126, fname, 'xxx'))
+        self.assertEqual(fname, _validate_file_name(_128, fname, 'xxx'))
+        fname = 'foo&bar.lp'
+        self.assertEqual(fname, _validate_file_name(_126, fname, 'xxx'))
+        self.assertEqual(fname, _validate_file_name(_128, fname, 'xxx'))
+        fname = 'foo~bar.lp'
+        self.assertEqual(fname, _validate_file_name(_126, fname, 'xxx'))
+        self.assertEqual(fname, _validate_file_name(_128, fname, 'xxx'))
+        fname = 'foo-bar.lp'
+        self.assertEqual(fname, _validate_file_name(_126, fname, 'xxx'))
+        self.assertEqual(fname, _validate_file_name(_128, fname, 'xxx'))
+
+        # Check unallowable character
+        bad_char = '^'
+        fname = 'foo%sbar.lp' % (bad_char,)
+        msg = r"Unallowed character \(\^\) found in CPLEX xxx file"
+        with self.assertRaisesRegex(ValueError, msg):
+            _validate_file_name(_126, fname, 'xxx')
+        with self.assertRaisesRegex(ValueError, msg):
+            _validate_file_name(_128, fname, 'xxx')
+
+
 
 class CPLEXShellWritePrioritiesFile(unittest.TestCase):
     """ Unit test on writing of priorities via `CPLEXSHELL._write_priorities_file()` """